home *** CD-ROM | disk | FTP | other *** search
/ Nejlepší hry / Nejlepsi hry.iso / hry / frozen bubble / fbinstaller.exe / {app} / src / frozen-bubble next >
Text File  |  2003-03-30  |  86KB  |  2,271 lines

  1. #!/usr/bin/perl
  2. #*****************************************************************************
  3. #
  4. #                          Frozen-Bubble
  5. #
  6. # Copyright (c) 2000, 2001, 2002, 2003 Guillaume Cottenceau <guillaume.cottenceau at free.fr>
  7. #
  8. # Sponsored by MandrakeSoft <http://www.mandrakesoft.com/>
  9. #
  10. # This program is free software; you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License version 2, as
  12. # published by the Free Software Foundation.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with this program; if not, write to the Free Software
  21. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  22. #
  23. #
  24. #******************************************************************************
  25. #
  26. # Design & Programming by Guillaume Cottenceau between Oct 2001 and Jan 2002.
  27. # Level Editor parts by Kim Joham and David Joham between Oct 2002 and Jan 2003
  28. #
  29. # Check official home: http://www.frozen-bubble.org/
  30. #
  31. #******************************************************************************
  32. #
  33. # Ported to Windows by Amir Szekely (http://sf.net/users/kichik)
  34. #
  35. #******************************************************************************
  36. # Yes it uses Perl, you non-believer :-).
  37. #
  38.  
  39. #use diagnostics;
  40. #use strict;
  41.  
  42. use vars qw($TARGET_ANIM_SPEED $BUBBLE_SIZE $ROW_SIZE $LAUNCHER_SPEED $BUBBLE_SPEED $MALUS_BUBBLE_SPEED $TIME_APPEARS_NEW_ROOT
  43.             %POS %POS_1P %POS_2P $KEYS %actions %angle %pdata $app $font %apprects $event %rects %sticked_bubbles %root_bubbles
  44.             $background $background_orig @bubbles_images $gcwashere %bubbles_anim %launched_bubble %tobe_launched %next_bubble
  45.             $shooter $sdl_flags $mixer $mixer_enabled $music_disabled $sfx_disabled @playlist %sound %music %pinguin %canon
  46.             $graphics_level @update_rects $CANON_ROTATIONS_NB %malus_bubble %falling_bubble %exploding_bubble %malus_gfx
  47.             %sticking_bubble $version $time %imgbin $TIME_HURRY_WARN $TIME_HURRY_MAX $TIMEOUT_PINGUIN_SLEEP $FREE_FALL_CONSTANT
  48.             $direct @PLAYERS %levels $display_on_app_disabled $total_time $time_1pgame $fullscreen $rcfile $hiscorefile $HISCORES
  49.             $lev_number $playermalus $loaded_levelset $direct_levelset $chainreaction %chains %history);
  50.  
  51. use Data::Dumper;
  52.  
  53. use SDL;
  54. use SDL::App;
  55. use SDL::Surface;
  56. use SDL::Event;
  57. use SDL::Cursor;
  58. use SDL::Font;
  59. use SDL::Mixer;
  60.  
  61. use fb_stuff;
  62. use fbsyms;
  63. use FBLE;
  64.  
  65. $| = 1;
  66.  
  67. $TARGET_ANIM_SPEED = 20;        # number of milliseconds that should last between two animation frames
  68. $LAUNCHER_SPEED = 0.03;        # speed of rotation of launchers
  69. $BUBBLE_SPEED = 10;        # speed of movement of launched bubbles
  70. $MALUS_BUBBLE_SPEED = 30;    # speed of movement of "malus" launched bubbles
  71. $CANON_ROTATIONS_NB = 40;       # number of rotations of images for canon (should be consistent with gfx/shoot/Makefile)
  72.  
  73. $TIMEOUT_PINGUIN_SLEEP = 200;
  74. $FREE_FALL_CONSTANT = 0.5;
  75. $KEYS = { p1 => { left => SDLK_x,    right => SDLK_v,     fire => SDLK_c,  center => SDLK_d },
  76.       p2 => { left => SDLK_LEFT, right => SDLK_RIGHT, fire => SDLK_UP, center => SDLK_DOWN },
  77.       misc => { fs => SDLK_f } };
  78.  
  79. $sdl_flags = SDL_ANYFORMAT | SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_HWACCEL | SDL_ASYNCBLIT;
  80. $mixer = 0;
  81. $graphics_level = 3;
  82. @PLAYERS = qw(p1 p2);
  83. $playermalus = 0;
  84. $chainreaction = 0;
  85.  
  86. $rcfile = "$FPATH/.fbrc";
  87. eval(cat_($rcfile));
  88. eval(cat_($hiscorefile = "$FPATH/.fbhighscores"));
  89.  
  90. $version = '1.0.0';
  91.  
  92. print "        [[ Frozen-Bubble-$version ]]\n\n";
  93. print '  http://www.frozen-bubble.org/
  94.  
  95.   Copyright (c) 2000, 2001, 2002, 2003 Guillaume Cottenceau.
  96.   Artwork: Alexis Younes <73lab at free.fr>
  97.            Amaury Amblard-Ladurantie <amaury at linuxfr.org>
  98.   Soundtrack: Matthias Le Bidan <matthias.le_bidan at caramail.com>
  99.   Design & Programming: Guillaume Cottenceau <guillaume.cottenceau at free.fr>
  100.   Level Editor: Kim and David Joham <[k|d]joham at yahoo.com>
  101.   Windows Port: Amir Szekely (http://sf.net/users/kichik)
  102.  
  103.   Sponsored by MandrakeSoft <http://www.mandrakesoft.com/>
  104.  
  105.   This program is free software; you can redistribute it and/or modify
  106.   it under the terms of the GNU General Public License version 2, as
  107.   published by the Free Software Foundation.
  108.  
  109. ';
  110.  
  111. local $_ = "@ARGV";
  112.  
  113. /-h/ and die "Usage: ", basename($0), " [OPTION]...
  114.   -h, --help                 display this help screen
  115.  -fs, --fullscreen           start in fullscreen mode (disabled)
  116.  -ns, --nosound              don't try to start any sound stuff
  117.  -nm, --nomusic              disable music (only)
  118.  -nfx, --nosfx               disable sound effects (only)
  119.       --playlist<directory>  use all files of the given directory as music files and play them
  120.  -sl, --slow_machine         enable slow machine mode (disable a few animations)
  121.  -vs, --very_slow_machine    enable very slow machine mode (disable all that can be disabled)
  122.  -di, --direct               directly start (2p) game (don't display menu)
  123.  -so, --solo                 directly start solo (1p) game, with random levels
  124.  -cr, --chain_reaction       enable chain-reaction
  125.  -l<#n>, --level<#n>         directly start the n-th level
  126.  -cb, --colourblind          use bubbles for colourblind people
  127.  -pm<#n>, --playermalus<#n>  add a malus of n to the left player (can be negative)
  128.  -ls<name>, --levelset<name> directly start with the specified levelset name
  129.  
  130. ";
  131.  
  132. /-fs/ || /-fu/ and $fullscreen = 0; # = 1;
  133. /-ns/ || /-noso/ and $mixer = 'SOUND_DISABLED';
  134. /-nm/ || /-nom/ and $music_disabled = 1;
  135. /-nfx/ || /-nosf/ and $sfx_disabled = 1;
  136. /-playlist\s*(\S+)/ and @playlist = glob("$1/*");
  137. /-sl/ and $graphics_level = 2;
  138. /-vs/ || /-ve/ and $graphics_level = 1;
  139. /-srand/ and srand 0;
  140. /-di/ and $direct = 1;
  141. /-so/ and $direct = 1, @PLAYERS = ('p1');
  142. /-cr/ || /-chain_reaction/ and $chainreaction = 1;
  143. /-cb/ || /-co/ and $colourblind = 1;
  144. /-pm\s*(-?[\d]+)/ || /-playermalus\s*(-?\d+)/ and $playermalus = $1;
  145. /-ls\s*(\S+)/ || /-levelset\s*(\S+)/ and $levels{current} = 1, $direct = 1, @PLAYERS = ('p1'), $direct_levelset = $1;
  146. /-l\s*(\d+)/ || /-level\s*(\d+)/ and $levels{current} = $1, $direct = 1, @PLAYERS = ('p1');
  147.  
  148.  
  149. #- ------------------------------------------------------------------------
  150.  
  151. END {
  152.     if ($app) {
  153.     $total_time = ($app->ticks - $total_time)/1000;
  154.     my $h = int($total_time/3600);
  155.     my $m = int(($total_time-$h*3600)/60);
  156.     my $s = int($total_time-$h*3600-$m*60);
  157.     print "\nAddicted for ", $h ? "$h"."h " : "", $m ? "$m"."m " : "", "$s"."s.\n";
  158.     }
  159. }
  160.  
  161. #- it doesn't keep ordering (but I don't care)
  162. sub fastuniq { my %l; @l{@_} = @_; values %l }
  163.  
  164.  
  165. #- ----------- sound related stuff ----------------------------------------
  166.  
  167. sub play_sound($) {
  168.     $mixer_enabled && $mixer && !$sfx_disabled && $sound{$_[0]} and $mixer->play_channel(-1, $sound{$_[0]}, 0);
  169. }
  170.  
  171. sub play_music($;$) {
  172.     my ($name, $pos) = @_;
  173.     $mixer_enabled && $mixer && !$music_disabled or return;
  174.     $app->delay(10) while $mixer->fading_music;   #- mikmod will deadlock if we try to fade_out while still fading in
  175.     $mixer->playing_music and $mixer->fade_out_music(500); $app->delay(400);
  176.     $app->delay(10) while $mixer->playing_music;  #- mikmod will segfault if we try to load a music while old one is still fading out
  177.     my %musics = (intro => '/snd/introzik.xm', main1p => '/snd/frozen-mainzik-1p.xm', main2p => '/snd/frozen-mainzik-2p.xm');
  178.     my $mus if 0;                                   #- I need to keep a reference on the music or it will be collected at the end of this function, thus I manually collect previous music
  179.     if (@playlist) {
  180.     my $tryanother = sub {
  181.         my $elem = shift @playlist;
  182.         $elem or return -1;
  183.         -f $elem or return 0;
  184.         $mus = SDL::Music->new($elem);
  185.         if ($mus->{-data}) {
  186.         print STDERR "[Playlist] playing `$elem'\n";
  187.         $mixer->play_music($mus, 0);
  188.         return 1;
  189.         } else { 
  190.         print STDERR "Warning, could not create new music from `$elem' (reason: ", $app->error, ").\n";
  191.         return 0;
  192.         }
  193.     };
  194.     while ($tryanother->() == 0) {};
  195.     } else {
  196.     $mus = SDL::Music->new("$FPATH$musics{$name}");
  197.     $mus->{-data} or print STDERR "Warning, could not create new music from `$FPATH$musics{$name}' (reason: ", $app->error, ").\n";
  198.     if ($pos) {
  199.         fb_c_stuff::fade_in_music_position($mus->{-data}, -1, 500, $pos);
  200.     } else {
  201.         $mixer->play_music($mus, -1);
  202.     }
  203.     }
  204. }
  205.  
  206. sub init_sound() {
  207.     $mixer = eval { SDL::Mixer->new(-frequency => 22050, -channels => 2, -size => 1024); };
  208.     if ($@) {
  209.     $@ =~ s| at \S+ line.*\n||;
  210.     print STDERR "\nWarning: can't initialize sound (reason: $@).\n";
  211.     return 0;
  212.     }
  213.     print "[Sound Init]\n";
  214.     my @sounds = qw(stick destroy_group newroot newroot_solo lose hurry pause menu_change menu_selected rebound launch malus noh snore cancel typewriter applause);
  215.     foreach (@sounds) {
  216.     my $sound_path = "$FPATH/snd/$_.wav";
  217.     $sound{$_} = SDL::Sound->new($sound_path);
  218.     if ($sound{$_}{-data}) {
  219.         $sound{$_}->volume(100);
  220.     } else {
  221.         print STDERR "Warning, could not create new sound from `$sound_path'.\n";
  222.     }
  223.     }
  224.     return 1;
  225. }
  226.  
  227.  
  228. #- ----------- graphics related stuff --------------------------------------
  229.  
  230. sub add_default_rect($) {
  231.     my ($surface) = @_;
  232.     $rects{$surface} = SDL::Rect->new(-width => $surface->width, -height => $surface->height);
  233. }
  234.  
  235. sub put_image($$$) {
  236.     my ($image, $x, $y) = @_;
  237.     $rects{$image} or die "please don't call me with no rects\n".backtrace();
  238.     my $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
  239.     $image->blit($rects{$image}, $app, $drect);
  240.     push @update_rects, $drect;
  241. }
  242.  
  243. sub erase_image_from($$$$) {
  244.     my ($image, $x, $y, $img) = @_;
  245.     my $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
  246.     $img->blit($drect, $app, $drect);
  247.     push @update_rects, $drect;
  248. }
  249.  
  250. sub erase_image($$$) {
  251.     my ($image, $x, $y) = @_;
  252.     erase_image_from($image, $x, $y, $background);
  253. }
  254.  
  255. sub put_image_to_background($$$) {
  256.     my ($image, $x, $y) = @_;
  257.     my $drect;
  258.     ($x == 0 && $y == 0) and print "put_image_to_background: warning, X and Y are 0\n";
  259.     if ($y > 0) {
  260.     $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
  261.     $display_on_app_disabled or $image->blit($rects{$image}, $app, $drect);
  262.     $image->blit($rects{$image}, $background, $drect);
  263.     } else {  #- clipping seems to not work when from one Surface to another Surface, so I need to do clipping by hand
  264.     $drect = SDL::Rect->new(-width => $image->width, -height => $image->height + $y, -x => $x, '-y' => 0);
  265.     my $irect = SDL::Rect->new(-width => $image->width, -height => $image->height + $y, '-y' => -$y);
  266.     $display_on_app_disabled or $image->blit($irect, $app, $drect);
  267.     $image->blit($irect, $background, $drect);
  268.     }
  269.     push @update_rects, $drect;
  270. }
  271.  
  272. sub remove_image_from_background($$$) {
  273.     my ($image, $x, $y) = @_;
  274.     ($x == 0 && $y == 0) and print "remove_image_from_background: warning, X and Y are 0\n";
  275.     my $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
  276.     $background_orig->blit($drect, $background, $drect);
  277.     $background_orig->blit($drect, $app, $drect);
  278.     push @update_rects, $drect;
  279. }
  280.  
  281. sub remove_images_from_background {
  282.     my ($player, @images) = @_;
  283.     foreach (@images) {
  284.     ($_->{'x'} == 0 && $_->{'y'} == 0) and print "remove_images_from_background: warning, X and Y are 0\n";
  285.     my $drect = SDL::Rect->new(-width => $_->{img}->width, -height => $_->{img}->height, -x => $_->{'x'}, '-y' => $_->{'y'});
  286.     $background_orig->blit($drect, $background, $drect);
  287.     $background_orig->blit($drect, $app, $drect);
  288.     push @update_rects, $drect;
  289.     }
  290. }
  291.  
  292. sub put_allimages_to_background($) {
  293.     my ($player) = @_;
  294.     put_image_to_background($_->{img}, $_->{'x'}, $_->{'y'}) foreach @{$sticked_bubbles{$player}};
  295. }
  296.  
  297. sub switch_image_on_background($$$;$) {
  298.     my ($image, $x, $y, $save) = @_;
  299.     my $drect = SDL::Rect->new(-width => $image->width, -height => $image->height, -x => $x, '-y' => $y);
  300.     if ($save) {
  301.     $save = SDL::Surface->new(-width => $image->width, -height => $image->height, -depth => 32, -Amask => "0 but true");  #- grrr... this piece of shit of Amask made the surfaces slightly modify along the print/erase of "Hurry" and "Pause".... took me so much time to debug and find that the problem came from a bug when Amask is set to 0xFF000000 (while it's -supposed- to be set to 0xFF000000 with 32-bit graphics!!)
  302.     $background->blit($drect, $save, $rects{$image});
  303.     }
  304.     $image->blit($rects{$image} || SDL::Rect->new(-width => $image->width, -height => $image->height), $background, $drect);
  305.     $background->blit($drect, $app, $drect);
  306.     push @update_rects, $drect;
  307.     return $save;
  308. }
  309.  
  310. sub add_image($) {
  311.     my $file = "$FPATH/gfx/$_[0]";
  312.     my $img = SDL::Surface->new(-name => $file);
  313.     if ($SDL::VERSION >= 1.20) {
  314.         $$img or die "FATAL: Couldn't load `$file' into a SDL::Surface.\n";
  315.     }
  316.     else {
  317.         $img->{-surface} or die "FATAL: Couldn't load `$file' into a SDL::Surface.\n";
  318.     }
  319.     add_default_rect($img);
  320.     return $img;
  321. }
  322.  
  323. sub add_bubble_image($) {
  324.     my ($file) = @_;
  325.     my $bubble = add_image($file);
  326.     push @bubbles_images, $bubble;
  327. }
  328.  
  329.  
  330. #- ----------- generic game stuff -----------------------------------------
  331.  
  332. sub iter_players(&) {
  333.     my ($f) = @_;
  334.     local $::p;
  335.     foreach $::p (@PLAYERS) {
  336.     &$f;
  337.     }
  338. }
  339. sub iter_players_(&) {  #- so that I can do an iter_players_ from within an iter_players
  340.     my ($f) = @_;
  341.     local $::p_;
  342.     foreach $::p_ (@PLAYERS) {
  343.     &$f;
  344.     }
  345. }
  346. sub is_1p_game() { @PLAYERS == 1 }
  347. sub is_2p_game() { @PLAYERS == 2 }
  348.  
  349.  
  350. #- ----------- bubble game stuff ------------------------------------------
  351.  
  352. sub calc_real_pos_given_arraypos($$$) {
  353.     my ($cx, $cy, $player) = @_;
  354.     ($POS{$player}{left_limit} + $cx * $BUBBLE_SIZE + odd($cy+$pdata{$player}{oddswap}) * $BUBBLE_SIZE/2,
  355.      $POS{top_limit} + $cy * $ROW_SIZE);
  356. }
  357.  
  358. sub calc_real_pos($$) {
  359.     my ($b, $player) = @_;
  360.     ($b->{'x'}, $b->{'y'}) = calc_real_pos_given_arraypos($b->{cx}, $b->{cy}, $player);
  361. }
  362.  
  363. sub get_array_yclosest($) {
  364.     my ($y) = @_;
  365.     return int(($y-$POS{top_limit}+$ROW_SIZE/2) / $ROW_SIZE);
  366. }
  367.  
  368. sub get_array_closest_pos($$$) { # roughly the opposite than previous function
  369.     my ($x, $y, $player) = @_;
  370.     my $ny = get_array_yclosest($y);
  371.     my $nx = int(($x-$POS{$player}{left_limit}+$BUBBLE_SIZE/2 - odd($ny+$pdata{$player}{oddswap})*$BUBBLE_SIZE/2)/$BUBBLE_SIZE);
  372.     return ($nx, $ny);
  373. }
  374.  
  375. sub is_collision($$$) {
  376.     my ($bub, $x, $y) = @_;
  377.     my $DISTANCE_COLLISION_SQRED = sqr($BUBBLE_SIZE * 0.82);
  378.     my $xs = sqr($bub->{x} - $x);
  379.     ($xs > $DISTANCE_COLLISION_SQRED) and return 0; 
  380.     return ($xs + sqr($bub->{'y'} - $y)) < $DISTANCE_COLLISION_SQRED;
  381. }
  382.  
  383. sub create_bubble_given_img($) {
  384.     my ($img) = @_;
  385.     my %bubble;
  386.     ref($img) eq 'SDL::Surface' or die "<$img> seems to not be a valid image\n" . backtrace();
  387.     $bubble{img} = $img;
  388.     return \%bubble;
  389. }
  390.  
  391. sub create_bubble(;$) {
  392.     my ($p) = @_;
  393.     my $b = create_bubble_given_img($bubbles_images[rand(@bubbles_images)]);
  394.     is_1p_game() && $p && !member($b->{img}, map { $_->{img} } @{$sticked_bubbles{$p}})
  395.       and return &create_bubble($p);  #- prototype checking pb w/ recursion
  396.     return $b;
  397. }
  398.  
  399. sub iter_rowscols(&$) {
  400.     my ($f, $oddswap) = @_;
  401.     local $::row; local $::col;
  402.     foreach $::row (0 .. 11) {
  403.     foreach $::col (0 .. 7 - odd($::row+$oddswap)) {
  404.         &$f;
  405.     }
  406.     }
  407. }
  408.  
  409. sub each_index(&@) {
  410.     my $f = shift;
  411.     local $::i = 0;
  412.     foreach (@_) {
  413.     &$f($::i);
  414.     $::i++;
  415.     }
  416. }
  417. sub img2numb { my ($i, $f) = @_; each_index { $i eq $_ and $f = $::i } @bubbles_images; return defined($f) ? $f : '-' }
  418.  
  419. #sub history {
  420. #    foreach my $frame (@{$history{$_[0]}}[-10...1]) {
  421. #    iter_rowscols {
  422. #        if ($::col == 0) {
  423. #        $::row == 0 or print "\n";
  424. #        odd($::row+$frame->{oddswap}) and print "  ";
  425. #        }
  426. #        foreach (@{$frame->{sticked}}) {
  427. #        $_->[0] == $::col && $_->[1] == $::row or next;
  428. #        print $_->[2];
  429. #        goto non_void;
  430. #        }
  431. #        if ($frame->{sticking}[0] == $::col && $frame->{sticking}[1] == $::row) {
  432. #        print "\033[D!$frame->{sticking}[2]";
  433. #        goto non_void;
  434. #        }
  435. #        print '-';
  436. #      non_void:
  437. #        $::col+odd($::row+$frame->{oddswap}) < 7 and print "   ";
  438. #        } $frame->{oddswap};
  439. #    print "\n\n";
  440. #    }
  441. #}
  442.  
  443. sub bubble_next_to($$$$$) {
  444.     my ($x1, $y1, $x2, $y2, $player) = @_;
  445.     $x1 == $x2 && $y1 == $y2 and die "bubble_next_to: assert failed -- same bubbles ($x1:$y1;$player)" . backtrace();
  446. #    $x1 == $x2 && $y1 == $y2 and history($player), die "bubble_next_to: assert failed -- same bubbles ($x1:$y1;$player)" . backtrace();
  447.     return to_bool((sqr($x1+odd($y1+$pdata{$player}{oddswap})*0.5 - ($x2+odd($y2+$pdata{$player}{oddswap})*0.5)) + sqr($y1 - $y2)) < 3);
  448. }
  449.  
  450. sub next_positions($$) {
  451.     my ($b, $player) = @_;
  452.     my $validate_pos = sub {
  453.     my ($x, $y) = @_;
  454.     if_($x >= 0 && $x+odd($y+$pdata{$player}{oddswap}) <= 7 && $y >= 0 && $y >= $pdata{$player}{newrootlevel} && $y <= 11,
  455.         [ $x, $y ]);
  456.     };
  457.     ($validate_pos->($b->{cx} - 1, $b->{cy}),
  458.      $validate_pos->($b->{cx} + 1, $b->{cy}),
  459.      $validate_pos->($b->{cx} - even($b->{cy}+$pdata{$player}{oddswap}), $b->{cy} - 1),
  460.      $validate_pos->($b->{cx} - even($b->{cy}+$pdata{$player}{oddswap}), $b->{cy} + 1),
  461.      $validate_pos->($b->{cx} - even($b->{cy}+$pdata{$player}{oddswap}) + 1, $b->{cy} - 1),
  462.      $validate_pos->($b->{cx} - even($b->{cy}+$pdata{$player}{oddswap}) + 1, $b->{cy} + 1));
  463. }
  464.  
  465. #- bubble ends its life sticked somewhere
  466. sub real_stick_bubble {
  467.     my ($bubble, $xpos, $ypos, $player, $neighbours_ok) = @_;
  468.     $bubble->{cx} = $xpos;
  469.     $bubble->{cy} = $ypos;
  470.     foreach (@{$sticked_bubbles{$player}}) {
  471.     if (bubble_next_to($_->{cx}, $_->{cy}, $bubble->{cx}, $bubble->{cy}, $player)) {
  472.         push @{$_->{neighbours}}, $bubble;
  473.         $neighbours_ok or push @{$bubble->{neighbours}}, $_;
  474.     }
  475.     }
  476.     push @{$sticked_bubbles{$player}}, $bubble;
  477.     $bubble->{cy} == $pdata{$player}{newrootlevel} and push @{$root_bubbles{$player}}, $bubble;
  478.     calc_real_pos($bubble, $player);
  479.     put_image_to_background($bubble->{img}, $bubble->{'x'}, $bubble->{'y'});
  480. }
  481.  
  482. sub destroy_bubbles {
  483.     my ($player, @bubz) = @_;
  484.     $graphics_level == 1 and return;
  485.     foreach (@bubz) {
  486.     $_->{speedx} = rand(3)-1.5;
  487.     $_->{speedy} = -rand(4)-2;
  488.     }
  489.     push @{$exploding_bubble{$player}}, @bubz;
  490. }
  491.  
  492. sub find_bubble_group($) {
  493.     my ($b) = @_;
  494.     my @neighbours = $b;
  495.     my @group;
  496.     while (1) {
  497.     push @group, @neighbours;
  498.     @neighbours = grep { $b->{img} eq $_->{img} && !member($_, @group) } fastuniq(map { @{$_->{neighbours}} } @neighbours);
  499.     last if !@neighbours;
  500.     }
  501.     @group;
  502. }
  503.  
  504. sub stick_bubble($$$$$) {
  505.     my ($bubble, $xpos, $ypos, $player, $count_for_root) = @_;
  506.     my @falling;
  507.     my $need_redraw = 0;
  508.     @{$bubble->{neighbours}} = grep { bubble_next_to($_->{cx}, $_->{cy}, $xpos, $ypos, $player) } @{$sticked_bubbles{$player}};
  509.  
  510.     #- in multiple chain reactions, it's possible that the group doesn't exist anymore in some rare situations :/
  511.     exists $bubble->{chaindestx} && !@{$bubble->{neighbours}} and return;
  512.  
  513.     my @will_destroy = difference2([ find_bubble_group($bubble) ], [ $bubble ]);
  514.  
  515.     if (@will_destroy <= 1) {
  516.     #- stick
  517.     play_sound('stick');
  518.     real_stick_bubble($bubble, $xpos, $ypos, $player, 1);
  519.     $sticking_bubble{$player} = $bubble;
  520.     $pdata{$player}{sticking_step} = 0;
  521.     } else {
  522.     #- destroy the group
  523.     play_sound('destroy_group');
  524.     foreach my $b (difference2([ fastuniq(map { @{$_->{neighbours}} } @will_destroy) ], \@will_destroy)) {
  525.         @{$b->{neighbours}} = difference2($b->{neighbours}, \@will_destroy);
  526.     }
  527.     @{$sticked_bubbles{$player}} = difference2($sticked_bubbles{$player}, \@will_destroy);
  528.     @{$root_bubbles{$player}} = difference2($root_bubbles{$player}, \@will_destroy);
  529.  
  530.     $bubble->{'cx'} = $xpos;
  531.     $bubble->{'cy'} = $ypos;
  532.     calc_real_pos($bubble, $player);
  533.     destroy_bubbles($player, @will_destroy, $bubble);
  534.  
  535.     #- find falling bubbles
  536.     $_->{mark} = 0 foreach @{$sticked_bubbles{$player}};
  537.     my @still_sticked;
  538.     my @neighbours = @{$root_bubbles{$player}};
  539.     my $distance_to_root;
  540.     while (1) {
  541.         $_->{mark} = ++$distance_to_root foreach @neighbours;
  542.         push @still_sticked, @neighbours;
  543.         @neighbours = grep { $_->{mark} == 0 } map { @{$_->{neighbours}} } @neighbours;
  544.         last if !@neighbours;
  545.     }
  546.     @falling = difference2($sticked_bubbles{$player}, \@still_sticked);
  547.     @{$sticked_bubbles{$player}} = difference2($sticked_bubbles{$player}, \@falling);
  548.  
  549.     #- chain-reaction on falling bubbles
  550.     if ($chainreaction) {
  551.         my @falling_colors = map { $_->{img} } @falling;
  552.         #- optimize a bit by first calculating bubbles that are next to another bubble of the same color
  553.         my @grouped_bubbles = grep {
  554.         my $b = $_;
  555.         member($b->{img}, @falling_colors) && any { $b->{img} eq $_->{img} } @{$b->{neighbours}}
  556.         } @{$sticked_bubbles{$player}};
  557.         if (@grouped_bubbles) {
  558.         #- all positions on which we can't chain-react
  559.         my @occupied_positions = map { $_->{cy}*8 + $_->{cx} } @{$sticked_bubbles{$player}};
  560.         push @occupied_positions, map { $_->{chaindestcy}*8 + $_->{chaindestcx} } @{$chains{$player}{falling_chained}};
  561.         #- examine groups beginning at the root bubbles, for the case in which
  562.         #- there is a group that will fall from an upper chain-reaction
  563.         foreach my $pos (sort { $a->{mark} <=> $b->{mark} } @grouped_bubbles) {
  564.             #- now examine if there is a free position to chain-react in it
  565.             foreach my $npos (next_positions($pos, $player)) {
  566.             #- we can't chain-react somewhere if it explodes a group already chained
  567.             next if any { $pos->{cx} == $_->{cx} && $pos->{cy} == $_->{cy} }
  568.                     map { @{$chains{$player}{chained_bubbles}{$_}}} keys %{$chains{$player}{chained_bubbles}};
  569.             if (!member($npos->[1]*8 + $npos->[0], @occupied_positions)) {
  570.                 #- find a suitable falling bubble for that free position
  571.                 foreach my $falling (@falling) {
  572.                 next if member($falling, @{$chains{$player}{falling_chained}});
  573.                 if ($pos->{img} eq $falling->{img}) {
  574.                     ($falling->{chaindestcx}, $falling->{chaindestcy}) = ($npos->[0], $npos->[1]);
  575.                     ($falling->{chaindestx}, $falling->{chaindesty}) = calc_real_pos_given_arraypos($npos->[0], $npos->[1], $player);
  576.                     push @{$chains{$player}{falling_chained}}, $falling;
  577.                     push @occupied_positions, $npos->[1]*8 + $npos->[0];
  578.                     
  579.                     #- next lines will allow not to chain-react on the same group from two different positions,
  580.                     #- and even to not chain-react on a group that will itself fall from a chain-reaction
  581.                     @{$falling->{neighbours}} = grep { bubble_next_to($_->{cx}, $_->{cy}, $npos->[0], $npos->[1], $player) } @{$sticked_bubbles{$player}};
  582.                     my @chained_bubbles = find_bubble_group($falling);
  583.                     $_->{mark} = 0 foreach @{$sticked_bubbles{$player}};
  584.                     my @still_sticked;
  585.                     my @neighbours = difference2($root_bubbles{$player}, \@chained_bubbles);
  586.                     while (1) {
  587.                     $_->{mark} = 1 foreach @neighbours;
  588.                     push @still_sticked, @neighbours;
  589.                     @neighbours = difference2([ grep { $_->{mark} == 0 } map { @{$_->{neighbours}} } @neighbours ],
  590.                                   \@chained_bubbles);
  591.                     last if !@neighbours;
  592.                     }
  593.                     @{$chains{$player}{chained_bubbles}{$falling}} = difference2($sticked_bubbles{$player}, \@still_sticked);
  594.                     last;
  595.                 }
  596.                 }
  597.             }
  598.             }
  599.         }
  600.         }
  601.     }
  602.  
  603.     #- prepare falling bubbles
  604.     if ($graphics_level > 1) {
  605.         my $max_cy_falling = fold_left { $::b->{cy} > $::a ? $::b->{cy} : $::a } 0, @falling;  #- I have a fold_left in my prog! :-)
  606.         my ($shift_on_same_line, $line) = (0, $max_cy_falling);
  607.         foreach (sort { $b->{cy}*8 + $b->{cx} <=> $a->{cy}*8 + $a->{cx} } @falling) {  #- sort bottom-to-up / right-to-left
  608.         $line != $_->{cy} and $shift_on_same_line = 0;
  609.         $line = $_->{cy};
  610.         $_->{wait_fall} = ($max_cy_falling - $_->{cy})*5 + $shift_on_same_line;
  611.         $shift_on_same_line++;
  612.         $_->{speed} = 0;
  613.         }
  614.         push @{$falling_bubble{$player}}, @falling;
  615.     }
  616.  
  617.     remove_images_from_background($player, @will_destroy, @falling);
  618.     #- redraw neighbours because parts of neighbours have been erased by previous statement
  619.     put_image_to_background($_->{img}, $_->{'x'}, $_->{'y'})
  620.       foreach grep { !member($_, @will_destroy) && !member($_, @falling) } fastuniq(map { @{$_->{neighbours}} } @will_destroy, @falling);
  621.     $need_redraw = 1;
  622.     }
  623.  
  624.     if ($count_for_root) {
  625.     $pdata{$player}{newroot}++;
  626.     if ($pdata{$player}{newroot} == $TIME_APPEARS_NEW_ROOT-1) {
  627.         $pdata{$player}{newroot_prelight} = 2;
  628.         $pdata{$player}{newroot_prelight_step} = 0;
  629.     }
  630.     if ($pdata{$player}{newroot} == $TIME_APPEARS_NEW_ROOT) {
  631.         $pdata{$player}{newroot_prelight} = 1;
  632.         $pdata{$player}{newroot_prelight_step} = 0;
  633.     }
  634.     if ($pdata{$player}{newroot} > $TIME_APPEARS_NEW_ROOT) {
  635.         $need_redraw = 1;
  636.         $pdata{$player}{newroot_prelight} = 0;
  637.         play_sound(is_1p_game() ? 'newroot_solo' : 'newroot');
  638.         $pdata{$player}{newroot} = 0;
  639.         $pdata{$player}{oddswap} = !$pdata{$player}{oddswap};
  640.         remove_images_from_background($player, @{$sticked_bubbles{$player}});
  641.         foreach (@{$sticked_bubbles{$player}}) {
  642.         $_->{'cy'}++;
  643.         calc_real_pos($_, $player);
  644.         }
  645.         foreach (@{$falling_bubble{$player}}) {
  646.         exists $_->{chaindestx} or next;
  647.         $_->{chaindestcy}++;
  648.         $_->{chaindesty} += $ROW_SIZE;
  649.         }
  650.         put_allimages_to_background($player);
  651.         if (is_1p_game()) {
  652.         $pdata{$player}{newrootlevel}++;
  653.         print_compressor();
  654.         } else {
  655.         @{$root_bubbles{$player}} = ();
  656.         real_stick_bubble(create_bubble($player), $_, 0, $player, 0) foreach (0..(7-$pdata{$player}{oddswap}));
  657.         }
  658.     }
  659.     }
  660.  
  661.     if ($need_redraw) {
  662.     my $malus_val = @will_destroy + @falling - 2;
  663.     $malus_val > 0 and $malus_val += ($player eq 'p1' ? $playermalus : -$playermalus);
  664.     $malus_val < 0 and $malus_val = 0;
  665.     $background->blit($apprects{$player}, $app, $apprects{$player});
  666.     malus_change($malus_val, $player);
  667.     }
  668.  
  669. #    push @{$history{$player}}, { sticking => [ $xpos, $ypos, img2numb($bubble->{img}) ],
  670. #                 oddswap => $pdata{$player}{oddswap},
  671. #                 sticked => [ map { [ $_->{cx}, $_->{cy}, img2numb($_->{img}) ] } @{$sticked_bubbles{$player}} ] };
  672. }
  673.  
  674. sub print_next_bubble($$;$) {
  675.     my ($img, $player, $not_on_top_next) = @_;
  676.     put_image_to_background($img, $next_bubble{$player}{'x'}, $next_bubble{$player}{'y'});
  677.     $not_on_top_next or put_image_to_background($bubbles_anim{on_top_next}, $POS{$player}{left_limit}+$POS{next_bubble}{x}-4, $POS{next_bubble}{'y'}-3);
  678. }
  679.  
  680. sub generate_new_bubble {
  681.     my ($player, $img) = @_;
  682.     $tobe_launched{$player} = $next_bubble{$player};
  683.     $tobe_launched{$player}{'x'} = ($POS{$player}{left_limit}+$POS{$player}{right_limit})/2 - $BUBBLE_SIZE/2;
  684.     $tobe_launched{$player}{'y'} = $POS{'initial_bubble_y'};
  685.     $next_bubble{$player} = $img ? create_bubble_given_img($img) : create_bubble($player);
  686.     $next_bubble{$player}{'x'} = $POS{$player}{left_limit}+$POS{next_bubble}{x}; #- necessary to keep coordinates, for verify_if_end
  687.     $next_bubble{$player}{'y'} = $POS{next_bubble}{'y'};
  688.     print_next_bubble($next_bubble{$player}{img}, $player);
  689. }
  690.  
  691.  
  692. #- ----------- game stuff -------------------------------------------------
  693.  
  694. sub handle_graphics($) {
  695.     my ($fun) = @_;
  696.  
  697.     iter_players {
  698.     #- bubbles
  699.     foreach ($launched_bubble{$::p}, if_($fun ne \&erase_image, $tobe_launched{$::p})) {
  700.         $_ and $fun->($_->{img}, $_->{'x'}, $_->{'y'});
  701.     }
  702.     if ($fun eq \&put_image && $pdata{$::p}{newroot_prelight}) {
  703.         if ($pdata{$::p}{newroot_prelight_step}++ > 30*$pdata{$::p}{newroot_prelight}) {
  704.         $pdata{$::p}{newroot_prelight_step} = 0;
  705.         }
  706.         if ($pdata{$::p}{newroot_prelight_step} <= 8) {
  707.         my $hurry_overwritten = 0;
  708.         foreach my $b (@{$sticked_bubbles{$::p}}) {
  709.             next if ($graphics_level == 1 && $b->{'cy'} > 0);  #- in low graphics, only prelight first row
  710.             $b->{'cx'}+1 == $pdata{$::p}{newroot_prelight_step} and put_image($b->{img}, $b->{'x'}, $b->{'y'});
  711.             $b->{'cx'} == $pdata{$::p}{newroot_prelight_step} and put_image($bubbles_anim{white}, $b->{'x'}, $b->{'y'});
  712.             $b->{'cy'} > 6 and $hurry_overwritten = 1;
  713.         }
  714.         $hurry_overwritten && $pdata{$::p}{hurry_save_img} and print_hurry($::p, 1);  #- hurry was potentially overwritten
  715.         }
  716.     }
  717.     if ($sticking_bubble{$::p} && $graphics_level > 1) {
  718.         my $b = $sticking_bubble{$::p};
  719.         if ($fun eq \&erase_image) {
  720.         put_image($b->{img}, $b->{'x'}, $b->{'y'});
  721.         } else {
  722.         if ($pdata{$::p}{sticking_step} == @{$bubbles_anim{stick}}) {
  723.             $sticking_bubble{$::p} = undef;
  724.         } else {
  725.             put_image(${$bubbles_anim{stick}}[$pdata{$::p}{sticking_step}], $b->{'x'}, $b->{'y'});
  726.             if ($pdata{$::p}{sticking_step_slowdown}) {
  727.             $pdata{$::p}{sticking_step}++;
  728.             $pdata{$::p}{sticking_step_slowdown} = 0;
  729.             } else {
  730.             $pdata{$::p}{sticking_step_slowdown}++;
  731.             }
  732.         }
  733.         }
  734.     }
  735.  
  736.     #- shooter
  737.     if ($graphics_level > 1) {
  738.         my $num = int($angle{$::p}*$CANON_ROTATIONS_NB/($PI/2) + 0.5)-$CANON_ROTATIONS_NB;
  739.         $fun->($canon{img}{$num},
  740.            ($POS{$::p}{left_limit}+$POS{$::p}{right_limit})/2 - 50 + $canon{data}{$num}->[0],
  741.            $POS{'initial_bubble_y'} + 16 - 50 + $canon{data}{$num}->[1] );  #- 50/50 stand for half width/height of gfx/shoot/base.png
  742.     } else {
  743.         $fun->($shooter,
  744.            ($POS{$::p}{left_limit}+$POS{$::p}{right_limit})/2 - 1 + 60*cos($angle{$::p}),  #- 1 for $shooter->width/2
  745.            $POS{'initial_bubble_y'} + 16 - 1 - 60*sin($angle{$::p}));  #- 1/1 stand for half width/height of gfx/shoot/shooter.png
  746.     }
  747.     #- penguins
  748.     if ($graphics_level == 3) {
  749.         $fun->($pinguin{$::p}{$pdata{$::p}{ping_right}{state}}[$pdata{$::p}{ping_right}{img}], $POS{$::p}{left_limit}+$POS{$::p}{pinguin}{x}, $POS{$::p}{pinguin}{'y'});
  750.     }
  751.  
  752.     #- moving bubbles --> I want them on top of the rest
  753.     foreach (@{$malus_bubble{$::p}}, @{$falling_bubble{$::p}}, @{$exploding_bubble{$::p}}) {
  754.         $fun->($_->{img}, $_->{'x'}, $_->{'y'});
  755.     }
  756.  
  757.     };
  758.  
  759. }
  760.  
  761. #- extract it from "handle_graphics" to optimize a bit animations
  762. sub malus_change($$) {
  763.     my ($numb, $player) = @_;
  764.     return if $numb == 0 || is_1p_game();
  765.     if ($numb >= 0) {
  766.     $player = ($player eq 'p1') ? 'p2' : 'p1';
  767.     }
  768.     my $update_malus = sub($) {
  769.     my ($fun) = @_;
  770.     my $malus = $pdata{$player}{malus};
  771.     my $y_shift = 0;
  772.     while ($malus > 0) {
  773.         my $print = sub($) {
  774.         my ($type) = @_;
  775.         $fun->($type, $POS{$player}{malus_x} - $type->width/2, $POS{'malus_y'} - $y_shift - $type->height);
  776.         $y_shift += $type->height - 1;
  777.         };
  778.         if ($malus >= 7) {
  779.         $print->($malus_gfx{tomate});
  780.         $malus -= 7;
  781.         } else {
  782.         $print->($malus_gfx{banane});
  783.         $malus--;
  784.         }
  785.     }
  786.     };
  787.     $update_malus->(\&remove_image_from_background);
  788.     $pdata{$player}{malus} += $numb;
  789.     $update_malus->(\&put_image_to_background);
  790. }
  791.  
  792. sub print_compressor() {
  793.     my $x = $POS{compressor_xpos};
  794.     my $y = $POS{top_limit} + $pdata{$PLAYERS[0]}{newrootlevel} * $ROW_SIZE;
  795.     my ($comp_main, $comp_ext) = ($imgbin{compressor_main}, $imgbin{compressor_ext});
  796.  
  797.     my $drect = SDL::Rect->new(-width => $comp_main->width, -height => $y,
  798.                    -x => $x - $comp_main->width/2, '-y' => 0);
  799.     $background_orig->blit($drect, $background, $drect);
  800.     $display_on_app_disabled or $background_orig->blit($drect, $app, $drect);
  801.     push @update_rects, $drect;
  802.  
  803.     put_image_to_background($comp_main, $x - $comp_main->width/2, $y - $comp_main->height);
  804.  
  805.     $y -= $comp_main->height - 3;
  806.  
  807.     while ($y > 0) {
  808.     put_image_to_background($comp_ext, $x - $comp_ext->width/2, $y - $comp_ext->height);
  809.     $y -= $comp_ext->height;
  810.     }
  811. }
  812.  
  813. sub handle_game_events() {
  814.     $event->pump;
  815.     if ($event->poll != 0) {
  816.     if ($event->type == SDL_KEYDOWN) {
  817.         my $keypressed = $event->key_sym;
  818.  
  819.         iter_players {
  820.         my $pkey = is_1p_game() ? 'p2' : $::p;
  821.         foreach (qw(left right fire center)) {
  822.             $keypressed == $KEYS->{$pkey}{$_} and $actions{$::p}{$_} = 1, last;
  823.         }
  824.         };
  825.         
  826.         if ($keypressed == $KEYS->{misc}{fs}) {
  827.         #$fullscreen = !$fullscreen;
  828.         #$app->fullscreen;
  829.         }
  830.  
  831.         if ($keypressed == SDLK_PAUSE) {
  832.         play_sound('pause');
  833.         $mixer_enabled && $mixer and $mixer->pause_music;
  834.         my $back_saved = switch_image_on_background($imgbin{back_paused}, 0, 0, 1);
  835.           pause_label:
  836.         while (1) {
  837.             my ($index, $side) = (0, 1);
  838.             while ($index || $side == 1) {
  839.             put_image(${$imgbin{paused}}[$index], $POS_1P{pause_clip}{x}, $POS_1P{pause_clip}{'y'});
  840.             $app->flip;
  841.             foreach (1..80) {
  842.                 $app->delay(20);
  843.                 $event->pump;
  844.                 if ($event->poll != 0 && $event->type == SDL_KEYDOWN) {
  845.                 last pause_label if $event->key_sym != $KEYS->{misc}{fs};
  846.                 #$fullscreen = !$fullscreen;
  847.                 #$app->fullscreen;
  848.                 }
  849.             }
  850.             rand() < 0.2 and play_sound('snore');
  851.             $index += $side;
  852.             if ($index == @{$imgbin{paused}}) {
  853.                 $side = -1;
  854.                 $index -= 2;
  855.             }
  856.             }
  857.         }
  858.         switch_image_on_background($back_saved, 0, 0);
  859.         iter_players { $actions{$::p}{left} = 0; $actions{$::p}{right} = 0; };
  860.         $mixer_enabled && $mixer and $mixer->resume_music;
  861.         $event->pump while $event->poll != 0;
  862.         $app->flip;
  863.         }
  864.  
  865.     }
  866.  
  867.     if ($event->type == SDL_KEYUP) {
  868.         my $keypressed = $event->key_sym;
  869.  
  870.         iter_players {
  871.         my $pkey = is_1p_game() ? 'p2' : $::p;
  872.         foreach (qw(left right fire center)) {
  873.             $keypressed == $KEYS->{$pkey}{$_} and $actions{$::p}{$_} = 0, last;
  874.         }
  875.         }
  876.     }
  877.  
  878.     if ($event->type == SDL_QUIT ||
  879.         $event->type == SDL_KEYDOWN && $event->key_sym == SDLK_ESCAPE) {
  880.         die 'quit';
  881.     }
  882.     }
  883. }
  884.  
  885. sub print_scores($) {
  886.     my ($surface) = @_;  #- TODO all this function has hardcoded coordinates
  887.     my $drect = SDL::Rect->new(-width => 120, -height => 30, -x => 260, '-y' => 428);
  888.     $background_orig->blit($drect, $surface, $drect);
  889.     push @update_rects, $drect;
  890.     iter_players_ {  #- sometimes called from within a iter_players so...
  891.     $surface->print($POS{$::p_}{scoresx}-SDL_TEXTWIDTH($pdata{$::p_}{score})/2, $POS{scoresy}, $pdata{$::p_}{score});
  892.     };
  893. }
  894.  
  895. sub verify_if_end {
  896.     iter_players {
  897.     if (any { $_->{cy} > 11 } @{$sticked_bubbles{$::p}}) {
  898.         $pdata{state} = "lost $::p";
  899.         play_sound('lose');
  900.         $pdata{$::p}{ping_right}{state} = 'lose';
  901.         $pdata{$::p}{ping_right}{img} = 0;
  902.         if (!is_1p_game()) {
  903.         my $won = $::p eq 'p1' ? 'p2' : 'p1';
  904.         $pdata{$won}{score}++;
  905.         $pdata{$won}{ping_right}{state} = 'win';
  906.         $pdata{$won}{ping_right}{img} = 0;
  907.         print_scores($background); print_scores($app);
  908.         }
  909.         foreach ($launched_bubble{$::p}, $tobe_launched{$::p}, @{$malus_bubble{$::p}}) {
  910.         $_ or next;
  911.         $_->{img} = $bubbles_anim{lose};
  912.         $_->{'x'}--;
  913.         $_->{'y'}--;
  914.         }
  915.         iter_players_ {
  916.         remove_hurry($::p_);
  917.         @{$falling_bubble{$::p_}} = grep { !exists $_->{chaindestx} } @{$falling_bubble{$::p_}};
  918.         };
  919.         print_next_bubble($bubbles_anim{lose}, $::p, 1);
  920.         iter_players_ {
  921.         @{$sticked_bubbles{$::p_}} = sort { $b->{'cx'}+$b->{'cy'}*10 <=> $a->{'cx'}+$a->{'cy'}*10 } @{$sticked_bubbles{$::p_}};
  922.         $sticking_bubble{$::p_} = undef;
  923.         $launched_bubble{$::p_} and destroy_bubbles($::p_, $launched_bubble{$::p_});
  924.         $launched_bubble{$::p_} = undef;
  925.         $pdata{$::p_}{newroot_prelight} = 0;
  926.         };
  927.         @{$malus_bubble{$::p}} = ();
  928.     }
  929.     };
  930.  
  931.     if (is_1p_game() && @{$sticked_bubbles{$PLAYERS[0]}} == 0) {
  932.     put_image_to_background($imgbin{win_panel_1player}, $POS{centerpanel}{x}, $POS{centerpanel}{'y'});
  933.     $pdata{state} = "won $PLAYERS[0]";
  934.     $pdata{$PLAYERS[0]}{ping_right}{state} = 'win';
  935.     $pdata{$PLAYERS[0]}{ping_right}{img} = 0;
  936.     $levels{current} and $levels{current}++;
  937.     if ($levels{current} && !$levels{$levels{current}}) {
  938.         $levels{current} = 'WON';
  939.         @{$falling_bubble{$PLAYERS[0]}} = @{$exploding_bubble{$PLAYERS[0]}} = ();
  940.         die 'quit';
  941.     }
  942.     }
  943. }
  944.  
  945. sub print_hurry($;$) {
  946.     my ($player, $dont_save_background) = @_;
  947.     my $t = switch_image_on_background($imgbin{hurry}{$player}, $POS{$player}{left_limit} + $POS{hurry}{x}, $POS{hurry}{'y'}, 1);
  948.     $dont_save_background or $pdata{$player}{hurry_save_img} = $t;
  949. }
  950. sub remove_hurry($) {
  951.     my ($player) = @_;
  952.     $pdata{$player}{hurry_save_img} and
  953.       switch_image_on_background($pdata{$player}{hurry_save_img}, $POS{$player}{left_limit} + $POS{hurry}{x}, $POS{hurry}{'y'});
  954.     $pdata{$player}{hurry_save_img} = undef;
  955. }
  956.  
  957.  
  958. #- ----------- mainloop helper --------------------------------------------
  959.  
  960. sub update_game() {
  961.  
  962.     if ($pdata{state} eq 'game') {
  963.     handle_game_events();
  964.     iter_players {
  965.         $actions{$::p}{left} and $angle{$::p} += $LAUNCHER_SPEED;
  966.         $actions{$::p}{right} and $angle{$::p} -= $LAUNCHER_SPEED;
  967.         if ($actions{$::p}{center}) {
  968.         if ($angle{$::p} >= $PI/2 - $LAUNCHER_SPEED
  969.             && $angle{$::p} <= $PI/2 + $LAUNCHER_SPEED) {
  970.             $angle{$::p} = $PI/2;
  971.         } else {
  972.             $angle{$::p} += ($angle{$::p} < $PI/2) ? $LAUNCHER_SPEED : -$LAUNCHER_SPEED;
  973.         }
  974.         }
  975.         ($angle{$::p} < 0.1) and $angle{$::p} = 0.1;
  976.         ($angle{$::p} > $PI-0.1) and $angle{$::p} = $PI-0.1;
  977.         $pdata{$::p}{hurry}++;
  978.         if ($pdata{$::p}{hurry} > $TIME_HURRY_WARN) {
  979.         my $oddness = odd(int(($pdata{$::p}{hurry}-$TIME_HURRY_WARN)/(500/$TARGET_ANIM_SPEED))+1);
  980.         if ($pdata{$::p}{hurry_oddness} xor $oddness) {
  981.             if ($oddness) {
  982.             play_sound('hurry');
  983.             print_hurry($::p);
  984.             } else {
  985.             remove_hurry($::p)
  986.             }
  987.         }
  988.         $pdata{$::p}{hurry_oddness} = $oddness;
  989.         }
  990.  
  991.         if (($actions{$::p}{fire} || $pdata{$::p}{hurry} == $TIME_HURRY_MAX)
  992.         && !$launched_bubble{$::p}
  993.         && !(any { exists $_->{chaindestx} } @{$falling_bubble{$::p}})
  994.         && !@{$malus_bubble{$::p}}) {
  995.         play_sound('launch');
  996.         $launched_bubble{$::p} = $tobe_launched{$::p};
  997.         $launched_bubble{$::p}->{direction} = $angle{$::p};
  998.         $tobe_launched{$::p} = undef;
  999.         $actions{$::p}{fire} = 0;
  1000.         $actions{$::p}{hadfire} = 1;
  1001.         $pdata{$::p}{hurry} = 0;
  1002.         remove_hurry($::p);
  1003.         }
  1004.  
  1005.         if ($launched_bubble{$::p}) {
  1006.         $launched_bubble{$::p}->{'x_old'} = $launched_bubble{$::p}->{'x'}; # save coordinates for potential collision
  1007.         $launched_bubble{$::p}->{'y_old'} = $launched_bubble{$::p}->{'y'};
  1008.         $launched_bubble{$::p}->{'x'} += $BUBBLE_SPEED * cos($launched_bubble{$::p}->{direction});
  1009.         $launched_bubble{$::p}->{'y'} -= $BUBBLE_SPEED * sin($launched_bubble{$::p}->{direction});
  1010.         if ($launched_bubble{$::p}->{x} < $POS{$::p}{left_limit}) {
  1011.             play_sound('rebound');
  1012.             $launched_bubble{$::p}->{x} = 2 * $POS{$::p}{left_limit} - $launched_bubble{$::p}->{x};
  1013.             $launched_bubble{$::p}->{direction} -= 2*($launched_bubble{$::p}->{direction}-$PI/2);
  1014.         }
  1015.         if ($launched_bubble{$::p}->{x} > $POS{$::p}{right_limit} - $BUBBLE_SIZE) {
  1016.             play_sound('rebound');
  1017.             $launched_bubble{$::p}->{x} = 2 * ($POS{$::p}{right_limit} - $BUBBLE_SIZE) - $launched_bubble{$::p}->{x};
  1018.             $launched_bubble{$::p}->{direction} += 2*($PI/2-$launched_bubble{$::p}->{direction});
  1019.         }
  1020.         if ($launched_bubble{$::p}->{'y'} <= $POS{top_limit} + $pdata{$::p}{newrootlevel} * $ROW_SIZE) {
  1021.             my ($cx, $cy) = get_array_closest_pos($launched_bubble{$::p}->{x}, $launched_bubble{$::p}->{'y'}, $::p);
  1022.             stick_bubble($launched_bubble{$::p}, $cx, $cy, $::p, 1);
  1023.             $launched_bubble{$::p} = undef;
  1024.         } else {
  1025.             foreach (@{$sticked_bubbles{$::p}}) {
  1026.             if (is_collision($launched_bubble{$::p}, $_->{'x'}, $_->{'y'})) {
  1027.                 my ($cx, $cy) = get_array_closest_pos(($launched_bubble{$::p}->{'x_old'}+$launched_bubble{$::p}->{'x'})/2,
  1028.                                   ($launched_bubble{$::p}->{'y_old'}+$launched_bubble{$::p}->{'y'})/2,
  1029.                                   $::p);
  1030.                 stick_bubble($launched_bubble{$::p}, $cx, $cy, $::p, 1);
  1031.                 $launched_bubble{$::p} = undef;
  1032.  
  1033.                 #- malus generation
  1034.                 if (!any { $_->{chaindestx} } @{$falling_bubble{$::p}}) {
  1035.                 $pdata{$::p}{malus} > 0 and play_sound('malus');
  1036.                 while ($pdata{$::p}{malus} > 0 && @{$malus_bubble{$::p}} < 7) {
  1037.                     my $b = create_bubble($::p);
  1038.                     do {
  1039.                     $b->{'cx'} = int(rand(7));
  1040.                     } while (member($b->{'cx'}, map { $_->{'cx'} } @{$malus_bubble{$::p}}));
  1041.                     $b->{'cy'} = 12;
  1042.                     $b->{'stick_y'} = 0;
  1043.                     foreach (@{$sticked_bubbles{$::p}}) {
  1044.                     if ($_->{'cy'} > $b->{'stick_y'}) {
  1045.                         if ($_->{'cx'} == $b->{'cx'}
  1046.                         || odd($_->{'cy'}+$pdata{$::p}{oddswap}) && ($_->{'cx'}+1) == $b->{'cx'}) {
  1047.                         $b->{'stick_y'} = $_->{'cy'};
  1048.                         }
  1049.                     }
  1050.                     }
  1051.                     $b->{'stick_y'}++;
  1052.                     calc_real_pos($b, $::p);
  1053.                     push @{$malus_bubble{$::p}}, $b;
  1054.                     malus_change(-1, $::p);
  1055.                 }
  1056.                 #- sort them and shift them
  1057.                 @{$malus_bubble{$::p}} = sort { $a->{'cx'} <=> $b->{'cx'} } @{$malus_bubble{$::p}};
  1058.                 my $shifting = 0;
  1059.                 $_->{'y'} += ($shifting+=7)+int(rand(20)) foreach @{$malus_bubble{$::p}};
  1060.                 }
  1061.  
  1062.                 last;
  1063.             }
  1064.             }
  1065.         }
  1066.         }
  1067.  
  1068.         !$tobe_launched{$::p} and generate_new_bubble($::p);
  1069.  
  1070.         if (!$actions{$::p}{left} && !$actions{$::p}{right} && !$actions{$::p}{hadfire}) {
  1071.         $pdata{$::p}{sleeping}++;
  1072.         } else {
  1073.         $pdata{$::p}{sleeping} = 0;
  1074.         $pdata{$::p}{ping_right}{movelatency} = -20;
  1075.         }
  1076.         if ($pdata{$::p}{sleeping} > $TIMEOUT_PINGUIN_SLEEP) {
  1077.         $pdata{$::p}{ping_right}{state} = 'sleep';
  1078.         } elsif ($pdata{$::p}{ping_right}{state} eq 'sleep') {
  1079.         $pdata{$::p}{ping_right}{state} = 'normal';
  1080.         }
  1081.         if ($pdata{$::p}{ping_right}{state} eq 'right' && !($actions{$::p}{right})
  1082.         || $pdata{$::p}{ping_right}{state} eq 'left' && !($actions{$::p}{left})
  1083.         || $pdata{$::p}{ping_right}{state} eq 'action' && ($pdata{$::p}{ping_right}{actionlatency}++ > 5)) {
  1084.         $pdata{$::p}{ping_right}{state} = 'normal';
  1085.         }
  1086.         $actions{$::p}{right} and $pdata{$::p}{ping_right}{state} = 'right';
  1087.         $actions{$::p}{left} and $pdata{$::p}{ping_right}{state} = 'left';
  1088.         if ($actions{$::p}{hadfire}) {
  1089.         $pdata{$::p}{ping_right}{state} = 'action';
  1090.         $actions{$::p}{hadfire} = 0;
  1091.         $pdata{$::p}{ping_right}{actionlatency} = 0;
  1092.         }
  1093.         if ($pdata{$::p}{ping_right}{state} eq 'normal' && ($pdata{$::p}{ping_right}{movelatency}++ > 10)) {
  1094.         $pdata{$::p}{ping_right}{movelatency} = 0;
  1095.         rand() < 0.4 and $pdata{$::p}{ping_right}{img} = int(rand(@{$pinguin{$::p}{normal}}));
  1096.         }
  1097.  
  1098.         if ($pdata{$::p}{ping_right}{img} >= @{$pinguin{$::p}{$pdata{$::p}{ping_right}{state}}}) {
  1099.         $pdata{$::p}{ping_right}{img} = 0;
  1100.         }
  1101.     };
  1102.  
  1103.     verify_if_end();
  1104.  
  1105.     } elsif ($pdata{state} =~ /lost (.*)/) {
  1106.     my $lost_slowdown if 0;  #- ``if 0'' is Perl's way of doing what C calls ``static local variables''
  1107.     if ($lost_slowdown++ > 1) {
  1108.         $lost_slowdown = 0;
  1109.         iter_players {
  1110.         if ($::p eq $1) {
  1111.             if (@{$sticked_bubbles{$::p}}) {
  1112.             my $b = shift @{$sticked_bubbles{$::p}};
  1113.             put_image_to_background($bubbles_anim{lose}, --$b->{'x'}, --$b->{'y'});
  1114.     #        my $line = $b->{'cy'};
  1115.     #        while (@{$sticked_bubbles{$::p}} && ${$sticked_bubbles{$::p}}[0]->{'cy'} == $line) {
  1116.     #            my $b = shift @{$sticked_bubbles{$::p}};
  1117.     #            put_image_to_background($bubbles_anim{lose}, --$b->{'x'}, --$b->{'y'});
  1118.     #        }
  1119.  
  1120.             if (@{$sticked_bubbles{$::p}} == 0) {
  1121.                 $graphics_level == 1 and put_image($imgbin{win}{$::p eq 'p1' ? 'p2' : 'p1'}, $POS{centerpanel}{x}, $POS{centerpanel}{'y'});
  1122.                 if (is_1p_game()) {
  1123.                 put_image($imgbin{lose}, $POS{centerpanel}{'x'}, $POS{centerpanel}{'y'});
  1124.                 play_sound('noh');
  1125.                 }
  1126.             }
  1127.  
  1128.             if (!@{$sticked_bubbles{$::p}}) {
  1129.                 $event->pump while $event->poll != 0;
  1130.             }
  1131.             } else {
  1132.             $event->pump;
  1133.             die 'new_game' if $event->poll != 0 && $event->type == SDL_KEYDOWN;
  1134.             }
  1135.         } else {
  1136.             if (@{$sticked_bubbles{$::p}} && $graphics_level > 1) {
  1137.             my $b = shift @{$sticked_bubbles{$::p}};
  1138.             destroy_bubbles($::p, $b);
  1139.             remove_image_from_background($b->{img}, $b->{'x'}, $b->{'y'});
  1140.             #- be sure to redraw at least upper line
  1141.             foreach (@{$b->{neighbours}}) {
  1142.                 next if !member($_, @{$sticked_bubbles{$::p}});
  1143.                 put_image_to_background($_->{img}, $_->{'x'}, $_->{'y'});
  1144.             }
  1145.             }
  1146.         }
  1147.         };
  1148.  
  1149.     }
  1150.  
  1151.     } elsif ($pdata{state} =~ /won (.*)/) {
  1152.     if (@{$exploding_bubble{$1}} == 0) {
  1153.         $event->pump;
  1154.         die 'new_game' if $event->poll != 0 && $event->type == SDL_KEYDOWN;
  1155.     }
  1156.  
  1157.     } else {
  1158.     die "oops unhandled game state ($pdata{state})\n";
  1159.     }
  1160.  
  1161.  
  1162.     #- things that need to be updated in all states of the game
  1163.     iter_players {
  1164.     my $malus_end = [];
  1165.     foreach my $b (@{$malus_bubble{$::p}}) {
  1166.         $b->{'y'} -= $MALUS_BUBBLE_SPEED;
  1167.         if (get_array_yclosest($b->{'y'}) <= $b->{'stick_y'}) {
  1168.         real_stick_bubble($b, $b->{'cx'}, $b->{'stick_y'}, $::p, 0);
  1169.         push @$malus_end, $b;
  1170.         }
  1171.     }
  1172.     @$malus_end and @{$malus_bubble{$::p}} = difference2($malus_bubble{$::p}, $malus_end);
  1173.  
  1174.     my $falling_end = [];
  1175.     foreach my $b (@{$falling_bubble{$::p}}) {
  1176.         if ($b->{wait_fall}) {
  1177.         $b->{wait_fall}--;
  1178.         } else {
  1179.         if (exists $b->{chaindestx} && ($b->{'y'} > 375 || $b->{chaingoingup})) {
  1180.             my $acceleration = $FREE_FALL_CONSTANT*3;
  1181.             if (!$b->{chaingoingup}) {
  1182.             my $time_to_zero = $b->{speed}/$acceleration;
  1183.             my $distance_to_zero = $b->{speed} * ($b->{speed}/$acceleration + 1) / 2;
  1184.             my $time_to_destination = (-1 + sqrt(1 + 8/$acceleration*($b->{'y'}-$b->{chaindesty}+$distance_to_zero))) / 2;
  1185.             $b->{speedx} = ($b->{chaindestx} - $b->{x}) / ($time_to_zero + $time_to_destination);
  1186.             $b->{chaingoingup} = 1;
  1187.             }
  1188.             $b->{speed} -= $acceleration;
  1189.             $b->{x} += $b->{speedx};
  1190.             if (abs($b->{x} - $b->{chaindestx}) < abs($b->{speedx})) {
  1191.             $b->{'x'} = $b->{chaindestx};
  1192.             $b->{speedx} = 0;
  1193.             }
  1194.             $b->{'y'} += $b->{speed};
  1195.             $b->{'y'} < $b->{chaindesty} and push @$falling_end, $b;
  1196.         } else {
  1197.             $b->{'y'} += $b->{speed};
  1198.             $b->{speed} += $FREE_FALL_CONSTANT;
  1199.         }
  1200.         }
  1201.         $b->{'y'} > 470 && !exists $b->{chaindestx} and push @$falling_end, $b;
  1202.     }
  1203.     @$falling_end and @{$falling_bubble{$::p}} = difference2($falling_bubble{$::p}, $falling_end);
  1204.     foreach (@$falling_end) {
  1205.         exists $_->{chaindestx} or next;
  1206.         @{$chains{$::p}{falling_chained}} = difference2($chains{$::p}{falling_chained}, [ $_ ]);
  1207.         delete $chains{$::p}{chained_bubbles}{$_};
  1208.         stick_bubble($_, $_->{chaindestcx}, $_->{chaindestcy}, $::p, 0);
  1209.     }
  1210.  
  1211.     my $exploding_end = [];
  1212.     foreach my $b (@{$exploding_bubble{$::p}}) {
  1213.         $b->{'x'} += $b->{speedx};
  1214.         $b->{'y'} += $b->{speedy};
  1215.         $b->{speedy} += $FREE_FALL_CONSTANT;
  1216.         push @$exploding_end, $b if $b->{'y'} > 470;
  1217.     }
  1218.     if (@$exploding_end) {
  1219.         @{$exploding_bubble{$::p}} = difference2($exploding_bubble{$::p}, $exploding_end);
  1220.         if ($pdata{state} =~ /lost (.*)/ && $::p ne $1 && !is_1p_game()
  1221.         && !@{$exploding_bubble{$::p}} && !@{$sticked_bubbles{$::p}}) {
  1222.         put_image($imgbin{win}{$::p}, $POS{centerpanel}{'x'}, $POS{centerpanel}{'y'});
  1223.         }
  1224.     }
  1225.  
  1226.     if (member($pdata{$::p}{ping_right}{state}, qw(win lose)) && ($pdata{$::p}{ping_right}{movelatency}++ > 5)) {
  1227.         my $state = $pdata{$::p}{ping_right}{state};
  1228.         $pdata{$::p}{ping_right}{movelatency} = 0;
  1229.         $pdata{$::p}{ping_right}{img}++;
  1230.         $pdata{$::p}{ping_right}{img} == @{$pinguin{$::p}{$state}}
  1231.           and $pdata{$::p}{ping_right}{img} = $pinguin{$::p}{"$state".'_roll_back_index'};
  1232.     }
  1233.  
  1234.     };
  1235. }
  1236.  
  1237. #- ----------- init stuff -------------------------------------------------
  1238.  
  1239. sub restart_app() {
  1240.     #$app = SDL::App->new(-flags => $sdl_flags | ($fullscreen ? SDL_FULLSCREEN : 0), -title => 'Frozen-Bubble', -width => 640, -height => 480);
  1241.     $app = SDL::App->new(-flags => $sdl_flags, -title => 'Frozen-Bubble', -icon => "gfx/pinguins/small_corner_open_p2.png", -width => 640, -height => 480);
  1242. }
  1243.  
  1244. sub print_step($) {
  1245.     my ($txt) = @_;
  1246.     print $txt;
  1247.     my $step if 0; $step ||= 0;
  1248.     put_image($imgbin{loading_step}, 100 + $step*12, 10);
  1249.     $app->flip;
  1250.     $step++;
  1251. }
  1252.  
  1253. sub load_levelset {
  1254.     my ($levelset_name) = @_;
  1255.  
  1256.     -e $levelset_name or die "No such levelset ($levelset_name).\n";
  1257.  
  1258.     $loaded_levelset = $levelset_name;
  1259.     my $row_numb = 0;
  1260.     my $curr_level = $levels{current};
  1261.  
  1262.     %levels = ();
  1263.     $levels{current} = $curr_level;
  1264.     $lev_number = 1;
  1265.  
  1266.     foreach my $line (cat_($levelset_name)) {
  1267.     if ($line !~ /\S/) {
  1268.         if ($row_numb) {
  1269.         $lev_number++;
  1270.         $row_numb = 0;
  1271.         }
  1272.     } else {
  1273.         my $col_numb = 0;
  1274.         foreach (split ' ', $line) {
  1275.         /-/ or push @{$levels{$lev_number}}, { cx => $col_numb, cy => $row_numb, img_num => $_ };
  1276.         $col_numb++;
  1277.         }
  1278.         $row_numb++;
  1279.     }
  1280.     }
  1281. }
  1282.  
  1283. sub init_game() {
  1284.     -r "$FPATH/$_" or die "[*ERROR*] the datafiles seem to be missing! (could not read `$FPATH/$_')\n".
  1285.                           "          The datafiles need to go to `$FPATH'.\n"
  1286.                 foreach qw(gfx snd data);
  1287.  
  1288.     print '[SDL Init] ';
  1289.     restart_app();
  1290.     $font = SDL::Font->new("$FPATH/gfx/font.png");
  1291.     $apprects{main} = SDL::Rect->new(-width => $app->width, -height => $app->height);
  1292.     $event = SDL::Event->new;
  1293.     $event->set_unicode(1);
  1294.     SDL::Cursor::show(0);
  1295.     $total_time = $app->ticks;
  1296.     $imgbin{loading} = add_image('loading.png');
  1297.     put_image($imgbin{loading}, 10, 10);
  1298.     $app->print(30, 60, uc("tip!  use '-h' on command-line to get more options"));
  1299.     $app->flip;
  1300.     $imgbin{loading_step} = add_image('loading_step.png');
  1301.  
  1302.     print_step('[Graphics');
  1303.     $imgbin{back_2p} = SDL::Surface->new(-name => "$FPATH/gfx/backgrnd.png");
  1304.     $imgbin{back_1p} = SDL::Surface->new(-name => "$FPATH/gfx/back_one_player.png");
  1305.     $background = SDL::Surface->new(-width => $app->width, -height => $app->height, -depth => 32, -Amask => '0 but true');
  1306.     $background_orig = SDL::Surface->new(-width => $app->width, -height => $app->height, -depth => 32, -Amask => '0 but true');
  1307.     $imgbin{backstartfull} = SDL::Surface->new(-name => "$FPATH/gfx/menu/back_start.png");
  1308.  
  1309.     print_step('.'); 
  1310.     add_bubble_image('balls/bubble-'.($colourblind && 'colourblind-')."$_.gif") foreach (1..8);
  1311.     $bubbles_anim{white} = add_image("balls/bubble_prelight.png");
  1312.     $bubbles_anim{lose} = add_image("balls/bubble_lose.png");
  1313.     $bubbles_anim{on_top_next} = add_image("on_top_next.png");
  1314.     push @{$bubbles_anim{stick}}, add_image("balls/stick_effect_$_.png") foreach (0..7);
  1315.  
  1316.     $shooter = add_image("shoot/shooter.png");
  1317.     $canon{img}{$_} = add_image("shoot/base_$_.png") foreach (-$CANON_ROTATIONS_NB..$CANON_ROTATIONS_NB);
  1318.     /(\S+) (\S+) (\S+)/ and $canon{data}{$1} = [ $2, $3 ] foreach cat_("$FPATH/gfx/shoot/data");  #- quantity of shifting needed (because of crop reduction)
  1319.     $malus_gfx{banane} = add_image('banane.png');
  1320.     $malus_gfx{tomate} = add_image('tomate.png');
  1321.  
  1322.     print_step('.'); 
  1323.     push @{$imgbin{paused}}, add_image("pause_$_.png") foreach 1..5;
  1324.     $imgbin{back_paused} = add_image('back_paused.png');
  1325.     $imgbin{lose} = add_image('lose_panel.png');
  1326.     $imgbin{win_panel_1player} = add_image('win_panel_1player.png');
  1327.     $imgbin{compressor_main} = add_image('compressor_main.png');
  1328.     $imgbin{compressor_ext} = add_image('compressor_ext.png');
  1329.  
  1330.     $imgbin{txt_1pgame_off}  = add_image('menu/txt_1pgame_off.png');
  1331.     $imgbin{txt_1pgame_over} = add_image('menu/txt_1pgame_over.png');
  1332.     $imgbin{txt_2pgame_off}  = add_image('menu/txt_2pgame_off.png');
  1333.     $imgbin{txt_2pgame_over} = add_image('menu/txt_2pgame_over.png');
  1334.     $imgbin{txt_editor_off}  = add_image('menu/txt_editor_off.png');
  1335.     $imgbin{txt_editor_over} = add_image('menu/txt_editor_over.png');
  1336.     $imgbin{txt_fullscreen_off}  = add_image('menu/txt_fullscreen_off.png');
  1337.     $imgbin{txt_fullscreen_over} = add_image('menu/txt_fullscreen_over.png');
  1338.     $imgbin{txt_fullscreen_act_off}  = add_image('menu/txt_fullscreen_act_off.png');
  1339.     $imgbin{txt_fullscreen_act_over} = add_image('menu/txt_fullscreen_act_over.png');
  1340.     $imgbin{txt_keys_off}  = add_image('menu/txt_keys_off.png');
  1341.     $imgbin{txt_keys_over} = add_image('menu/txt_keys_over.png');
  1342.     $imgbin{txt_sound_off}  = add_image('menu/txt_sound_off.png');
  1343.     $imgbin{txt_sound_over} = add_image('menu/txt_sound_over.png');
  1344.     $imgbin{txt_sound_act_off}  = add_image('menu/txt_sound_act_off.png');
  1345.     $imgbin{txt_sound_act_over} = add_image('menu/txt_sound_act_over.png');
  1346.     $imgbin{txt_graphics_1_off}  = add_image('menu/txt_graphics_1_off.png');
  1347.     $imgbin{txt_graphics_1_over} = add_image('menu/txt_graphics_1_over.png');
  1348.     $imgbin{txt_graphics_2_off}  = add_image('menu/txt_graphics_2_off.png');
  1349.     $imgbin{txt_graphics_2_over} = add_image('menu/txt_graphics_2_over.png');
  1350.     $imgbin{txt_graphics_3_off}  = add_image('menu/txt_graphics_3_off.png');
  1351.     $imgbin{txt_graphics_3_over} = add_image('menu/txt_graphics_3_over.png');
  1352.     $imgbin{txt_highscores_off}  = add_image('menu/txt_highscores_off.png');
  1353.     $imgbin{txt_highscores_over} = add_image('menu/txt_highscores_over.png');
  1354.     $imgbin{void_panel} = add_image('menu/void_panel.png');
  1355.     $imgbin{version} = add_image('menu/version.png');
  1356.  
  1357.     $imgbin{back_hiscores} = add_image('back_hiscores.png');
  1358.     $imgbin{hiscore_frame} = add_image('hiscore_frame.png');
  1359.  
  1360.     $imgbin{banner_artwork} = add_image('menu/banner_artwork.png');
  1361.     $imgbin{banner_soundtrack} = add_image('menu/banner_soundtrack.png');
  1362.     $imgbin{banner_cpucontrol} = add_image('menu/banner_cpucontrol.png');
  1363.     $imgbin{banner_leveleditor} = add_image('menu/banner_leveleditor.png');
  1364.     
  1365.     print_step('.'); 
  1366.     #- temporarily desactivate the intro storyboard because it's not finished yet
  1367.     #- $imgbin{frozen} = add_image('intro/txt_frozen.png');
  1368.     #- $imgbin{bubble} = add_image('intro/txt_bubble.png');
  1369.     #- $imgbin{intro_penguin_imgs}->{$_} = add_image("intro/intro_$_.png") foreach 1..19;
  1370.  
  1371.     local @PLAYERS = qw(p1 p2);  #- load all images even if -so commandline option was passed
  1372.     iter_players {
  1373.     $imgbin{hurry}{$::p} = add_image("hurry_$::p.png");
  1374.     $pinguin{$::p}{normal} = [ map { add_image($_) } ("pinguins/base_$::p.png", map { "pinguins/base_$::p"."_extra_0$_.png" } (1..3)) ];
  1375.     $pinguin{$::p}{sleep} = [ add_image("pinguins/sleep_$::p.png") ];
  1376.     $pinguin{$::p}{left} = [ add_image("pinguins/move_left_$::p.png") ];
  1377.     $pinguin{$::p}{right} = [ add_image("pinguins/move_right_$::p.png") ];
  1378.     $pinguin{$::p}{action} = [ add_image("pinguins/action_$::p.png") ];
  1379.     $pinguin{$::p}{win} = [ map { add_image("pinguins/$::p"."_win_$_.png") } qw(1 2 3 4 5 6 7 8 6) ];
  1380.     $pinguin{$::p}{win_roll_back_index} = 4;
  1381.     $pinguin{$::p}{lose} = [ map { add_image("pinguins/$::p"."_loose_$_.png") } qw(1 2 3 4 5 6 7 8 9) ];
  1382.     $pinguin{$::p}{lose_roll_back_index} = 5;
  1383.     $pinguin{$::p}{win} = [ map { add_image("pinguins/$::p"."_win_$_.png") } qw(1 2 3 4 5 6 7 8 6) ];
  1384.     $pinguin{$::p}{walkright} = [ map { add_image("pinguins/$::p"."_dg_walk_0$_.png") } qw(1 2 3 4 5 6) ];
  1385.     $imgbin{win}{$::p} = add_image("win_panel_$::p.png");
  1386.     $pdata{$::p}{score} = 0;
  1387.     };
  1388.     print_step('] '); 
  1389.  
  1390.     $lev_number = 0;
  1391.     print_step("[Levels] "); 
  1392.     load_levelset("$FPATH/data/levels");
  1393.  
  1394.     if ($mixer eq 'SOUND_DISABLED') {
  1395.     $mixer_enabled = $mixer = undef;
  1396.     } else {
  1397.     $mixer_enabled = init_sound();
  1398.     }
  1399.  
  1400.     fb_c_stuff::init_effects($FPATH);
  1401.  
  1402.     print "Ready.\n";
  1403. }
  1404.  
  1405. sub open_level($) {
  1406.     my ($level) = @_;
  1407.  
  1408.     $level eq 'WON' and $level = $lev_number;
  1409.  
  1410.     @{$levels{$level}} or die "No such level or void level ($level).\n";
  1411.     foreach my $l (@{$levels{$level}}) {
  1412.     iter_players {
  1413.         my $img = $l->{img_num} =~ /^\d+$/ ? $bubbles_images[$l->{img_num}] : $bubbles_anim{lose};
  1414.         real_stick_bubble(create_bubble_given_img($img), $l->{cx}, $l->{cy}, $::p, 0);
  1415.     };
  1416.     }
  1417. }
  1418.  
  1419. sub grab_key($) {
  1420.     my ($unicode) = @_;
  1421.     my $keyp;
  1422.     do {
  1423.     $event->wait;
  1424.     if ($event->type == SDL_KEYDOWN) {
  1425.         $keyp = $unicode ? ($event->key_unicode || $event->key_sym) : $event->key_sym;
  1426.     }
  1427.     } while ($event->type != SDL_KEYDOWN);
  1428.     do { $event->wait } while ($event->type != SDL_KEYUP);
  1429.     return $keyp;
  1430. }
  1431.  
  1432. sub display_highscores() {
  1433.  
  1434.     $imgbin{back_hiscores}->blit($apprects{main}, $app, $apprects{main});
  1435.  
  1436.     $display_on_app_disabled = 1;
  1437.     @PLAYERS = ('p1');
  1438.     %POS = %POS_1P;
  1439.     $POS{top_limit} = $POS{init_top_limit};
  1440.  
  1441.     my $initial_high_posx = 90;
  1442.     my ($high_posx, $high_posy) = ($initial_high_posx, 68);
  1443.     my $high_rect = SDL::Rect->new('-x' => $POS{p1}{left_limit} & 0xFFFFFFFC, '-y' => $POS{top_limit} & 0xFFFFFFFC,
  1444.                    '-width' => ($POS{p1}{right_limit}-$POS{p1}{left_limit}) & 0xFFFFFFFC, -height => ($POS{'initial_bubble_y'}-$POS{top_limit}-10) & 0xFFFFFFFC);
  1445.  
  1446.     $font = SDL::Font->new("$FPATH/gfx/font-hi.png");
  1447.     my $centered_print = sub($$$) {
  1448.     my ($x, $y, $txt) = @_;
  1449.     $app->print($x+($imgbin{hiscore_frame}->width-SDL_TEXTWIDTH(uc($txt)))/2 - 6,
  1450.             $y+$imgbin{hiscore_frame}->height - 8, uc($txt));
  1451.     };
  1452.  
  1453.     my $old_levelset = $loaded_levelset;
  1454.  
  1455.     foreach my $high (ordered_highscores()) {
  1456.     iter_players {
  1457.         @{$sticked_bubbles{$::p}} = ();
  1458.         @{$root_bubbles{$::p}} = ();
  1459.         $pdata{$::p}{newrootlevel} = 0;
  1460.         $pdata{$::p}{oddswap} = 0;
  1461.     };
  1462.     $imgbin{back_1p}->blit($high_rect, $background, $high_rect);
  1463.  
  1464.     # try to get it from the default-levelset. If we can't, default to the
  1465.     # last level in the default levelset
  1466.     if (!$high->{piclevel}) {
  1467.         $loaded_levelset ne "$FPATH/data/levels" and load_levelset("$FPATH/data/levels");
  1468.         
  1469.         # handle the case where the user has edited/created a levelset with more levels
  1470.         # than the default levelset and then got a high score
  1471.         if ($high->{level} > $lev_number) {
  1472.         open_level($lev_number);
  1473.         } else {
  1474.         open_level($high->{level});
  1475.         }
  1476.     } else {
  1477.         # this is the normal case. just load the level that the file tells us
  1478.         if ($loaded_levelset ne "$FPATH/.fbhighlevelshistory") {
  1479.         load_levelset("$FPATH/.fbhighlevelshistory");
  1480.         }
  1481.         open_level($high->{piclevel});
  1482.     }
  1483.  
  1484.     put_image($imgbin{hiscore_frame}, $high_posx - 7, $high_posy - 6);
  1485.     if ($SDL::VERSION >= 1.20) {
  1486.         fb_c_stuff::shrink($$app, ${($background)->display_format()}, $high_posx, $high_posy, $$high_rect, 4);
  1487.     }
  1488.     else {
  1489.         fb_c_stuff::shrink($app->{-surface}, $background->display_format->{-surface}, $high_posx, $high_posy, $high_rect->{-rect}, 4);
  1490.     }
  1491.     $centered_print->($high_posx, $high_posy,    $high->{name});
  1492.     $centered_print->($high_posx, $high_posy+20, $high->{level} eq 'WON' ? "WON!" : "LVL-".$high->{level});
  1493.     my $min = int($high->{time}/60);
  1494.     my $sec = int($high->{time} - $min*60); length($sec) == 1 and $sec = "0$sec";
  1495.     $centered_print->($high_posx, $high_posy+40, "$min'$sec''");
  1496.     $high_posx += 98;
  1497.     $high_posx > 550 and $high_posx = $initial_high_posx, $high_posy += 175;
  1498.     $high_posy > 440 and last;
  1499.     }
  1500.     load_levelset($old_levelset);
  1501.     $app->flip;
  1502.     $display_on_app_disabled = 0;
  1503.  
  1504.     $font = SDL::Font->new("$FPATH/gfx/font.png");
  1505.     $event->pump while ($event->poll != 0);
  1506.     grab_key(0);
  1507. }
  1508.  
  1509. sub keysym_to_char($) { my ($key) = @_; eval("$key eq SDLK_$_") and return uc($_) foreach @fbsyms::syms }
  1510.  
  1511. sub ask_from($) {
  1512.     my ($w) = @_;
  1513.     # $w->{intro} = [ 'text_intro_line1', 'text_intro_line2', ... ]
  1514.     # $w->{entries} = [ { q => 'question1?', a => \$var_answer1, f => 'flags' }, {...} ]   flags: ONE_CHAR
  1515.     # $w->{outro} = 'text_outro_uniline'
  1516.     # $w->{erase_background} = $background_right_one
  1517.  
  1518.     my $xpos_panel = (640-$imgbin{void_panel}->width)/2;
  1519.     my $ypos_panel = (480-$imgbin{void_panel}->height)/2;
  1520.     put_image($imgbin{void_panel}, $xpos_panel, $ypos_panel);
  1521.  
  1522.     my $xpos;
  1523.     my $ypos = $ypos_panel + 5;
  1524.  
  1525.     foreach my $i (@{$w->{intro}}) {
  1526.     if ($i) {
  1527.         my $xpos = (640-SDL_TEXTWIDTH($i))/2;
  1528.         $app->print($xpos, $ypos, $i);
  1529.     }
  1530.     $ypos += 22;
  1531.     }
  1532.  
  1533.     $ypos += 3;
  1534.  
  1535.     my $ok = 1;
  1536.   entries:
  1537.     foreach my $entry (@{$w->{entries}}) {
  1538.     $xpos = (640-$imgbin{void_panel}->width)/2 + 120 - SDL_TEXTWIDTH($entry->{'q'})/2;
  1539.     $app->print($xpos, $ypos, $entry->{'q'});
  1540.     $app->flip;
  1541.     my $srect_mulchar_redraw = SDL::Rect->new(-width => $imgbin{void_panel}->width, -height => 30,
  1542.                          -x => $xpos + 140 - $xpos_panel, '-y' => $ypos - $ypos_panel);
  1543.     my $drect_mulchar_redraw = SDL::Rect->new(-width => $imgbin{void_panel}->width, -height => 30,
  1544.                          -x => $xpos + 140, '-y' => $ypos);
  1545.     my $txt;
  1546.     while (1) {
  1547.         my $k = grab_key($entry->{f} !~ 'ONE_CHAR');
  1548.         $k == SDLK_ESCAPE and $ok = 0, last entries;
  1549.         play_sound('typewriter');
  1550.         if ($entry->{f} =~ 'ONE_CHAR' || $k != SDLK_RETURN) {
  1551.         my $x_echo = (640-$imgbin{void_panel}->width)/2 + 230;
  1552.         if ($entry->{f} =~ 'ONE_CHAR') {
  1553.             $txt = $k;
  1554.             $app->print($x_echo, $ypos, keysym_to_char($k));
  1555.         } else {
  1556.             $k = keysym_to_char($k);
  1557.             length($k) == 1 && length($txt) < 8 and $txt .= $k;
  1558.             member($k, qw(BACKSPACE DELETE LEFT)) and $txt =~ s/.$//;
  1559.             $imgbin{void_panel}->blit($srect_mulchar_redraw, $app, $drect_mulchar_redraw);
  1560.             $app->print($x_echo, $ypos, $txt);
  1561.         }
  1562.         $app->flip;
  1563.         }
  1564.         $entry->{f} =~ 'ONE_CHAR' || $k == SDLK_RETURN and last;
  1565.     }
  1566.     $entry->{answer} = $txt;
  1567.     $ypos += 22;
  1568.     }
  1569.  
  1570.     if ($ok) {
  1571.     ${$_->{a}} = $_->{answer} foreach @{$w->{entries}};
  1572.     $xpos = (640-SDL_TEXTWIDTH($w->{outro}))/2;
  1573.     $ypos = (480+$imgbin{void_panel}->height)/2 - 35;
  1574.     $app->print($xpos, $ypos, $w->{outro});
  1575.     $app->flip;
  1576.     play_sound('menu_selected');
  1577.     sleep 1;
  1578.     } else {
  1579.     play_sound('cancel');
  1580.     }
  1581.  
  1582.     exists $w->{erase_background} and erase_image_from($imgbin{void_panel}, $xpos_panel, $ypos_panel, $w->{erase_background});
  1583.     $app->flip;
  1584.     $event->pump while ($event->poll != 0);
  1585. }
  1586.  
  1587. sub new_game() {
  1588.  
  1589.     $display_on_app_disabled = 1;
  1590.  
  1591.     my $backgr;
  1592.     if (is_2p_game()) {
  1593.     $backgr = $imgbin{back_2p};
  1594.     %POS = %POS_2P;
  1595.     $TIME_APPEARS_NEW_ROOT = 11;
  1596.     $TIME_HURRY_WARN = 250;
  1597.     $TIME_HURRY_MAX = 375;
  1598.     } elsif (is_1p_game()) {
  1599.     $backgr = $imgbin{back_1p};
  1600.     %POS = %POS_1P;
  1601.     $TIME_APPEARS_NEW_ROOT = 8;
  1602.     $TIME_HURRY_WARN = 400;
  1603.     $TIME_HURRY_MAX = 525;
  1604.     $POS{top_limit} = $POS{init_top_limit};
  1605.     $pdata{$PLAYERS[0]}{score} = $levels{current} || "RANDOM";
  1606.     } else {
  1607.     die "oops";
  1608.     }
  1609.  
  1610.     $backgr->blit($apprects{main}, $background_orig, $apprects{main});
  1611.     $background_orig->blit($apprects{main}, $background, $apprects{main});
  1612.  
  1613.     iter_players {
  1614.     $actions{$::p}{$_} = 0 foreach qw(left right fire center);
  1615.     $angle{$::p} = $PI/2;
  1616.     @{$sticked_bubbles{$::p}} = ();
  1617.     @{$malus_bubble{$::p}} = ();
  1618.     @{$root_bubbles{$::p}} = ();
  1619.     @{$falling_bubble{$::p}} = ();
  1620.     @{$exploding_bubble{$::p}} = ();
  1621.     @{$chains{$::p}{falling_chained}} = ();
  1622.     %{$chains{$::p}{chained_bubbles}} = ();
  1623.     $launched_bubble{$::p} = undef;
  1624.     $sticking_bubble{$::p} = undef;
  1625.     $pdata{$::p}{$_} = 0 foreach qw(newroot newroot_prelight oddswap malus hurry newrootlevel);
  1626.     $pdata{$::p}{ping_right}{img} = 0;
  1627.     $pdata{$::p}{ping_right}{state} = 'normal';
  1628.     $apprects{$::p} = SDL::Rect->new('-x' => $POS{$::p}{left_limit}, '-y' => $POS{top_limit},
  1629.                      -width => $POS{$::p}{right_limit}-$POS{$::p}{left_limit}, -height => $POS{'initial_bubble_y'}-$POS{top_limit});
  1630.     };
  1631.     print_scores($background);
  1632.  
  1633.     is_1p_game() and print_compressor();
  1634.  
  1635.     if ($levels{current}) {
  1636.     open_level($levels{current});
  1637.     } else {
  1638.     foreach my $cy (0 .. 4) {
  1639.         foreach my $cx (0 .. (6 + even($cy))) {
  1640.         my $b = create_bubble();
  1641.         real_stick_bubble($b, $cx, $cy, $PLAYERS[0], 0);  #- this doesn't map well to the 'iter_players' subroutine..
  1642.         is_2p_game() and real_stick_bubble(create_bubble_given_img($b->{img}), $cx, $cy, $PLAYERS[1], 0);
  1643.         }
  1644.     }
  1645.     }
  1646.  
  1647.     $next_bubble{$PLAYERS[0]} = create_bubble($PLAYERS[0]);
  1648. #    $next_bubble{$PLAYERS[0]} = create_bubble_given_img($bubbles_images[5]);
  1649.     generate_new_bubble($PLAYERS[0]);
  1650.     if (is_2p_game()) {
  1651.     $next_bubble{$PLAYERS[1]} = create_bubble_given_img($tobe_launched{$PLAYERS[0]}->{img});
  1652.     generate_new_bubble($PLAYERS[1], $next_bubble{$PLAYERS[0]}->{img});
  1653.     }
  1654.  
  1655.     if ($graphics_level == 1) {
  1656.     $background->blit($apprects{main}, $app, $apprects{main});
  1657.     $app->flip;
  1658.     } else {
  1659.         if ($SDL::VERSION >= 1.20) {
  1660.             fb_c_stuff::effect($$app, ${($background)->display_format()});
  1661.         }
  1662.         else {
  1663.             fb_c_stuff::effect($app->{-surface}, $background->display_format->{-surface});
  1664.         }
  1665.     }
  1666.  
  1667.     $display_on_app_disabled = 0;
  1668.  
  1669.     $mixer_enabled && $mixer && @playlist && !$mixer->playing_music and play_music('dummy', 0);
  1670.  
  1671.     $event->pump while ($event->poll != 0);
  1672.     $pdata{state} = 'game';
  1673. }
  1674.  
  1675. sub new_game_once() {
  1676.     is_1p_game() && $levels{current} and choose_levelset();
  1677.     if (is_2p_game() && $graphics_level > 1) {
  1678.     my $answ;
  1679.     ask_from({ intro => [ '2-PLAYER GAME', '', '', 'ENABLE CHAIN-REACTION?', '' ],
  1680.            entries => [ { 'q' => 'Y OR N?', 'a' => \$answ, f => 'ONE_CHAR' } ],
  1681.            outro => 'ENJOY THE GAME!' });
  1682.     $chainreaction = $answ == SDLK_y; #;;
  1683.     }
  1684.     play_music(is_1p_game() ? 'main1p' : 'main2p');
  1685. }
  1686.  
  1687. sub lvl_cmp($$) { $_[0] eq 'WON' ? ($_[1] eq 'WON' ? 0 : 1) : ($_[1] eq 'WON' ? -1 : $_[0] <=> $_[1]) }
  1688.  
  1689. sub ordered_highscores() { return sort { lvl_cmp($b->{level}, $a->{level}) || $a->{time} <=> $b->{time} } @$HISCORES }
  1690.  
  1691. sub handle_new_hiscores() {
  1692.     is_1p_game() && $levels{current} or return;
  1693.  
  1694.     my @ordered = ordered_highscores();
  1695.     my $worst = pop @ordered;
  1696.  
  1697.     my $total_seconds = ($app->ticks - $time_1pgame)/1000;
  1698.  
  1699.     if (@$HISCORES == 10 && (lvl_cmp($levels{current}, $worst->{level}) == -1
  1700.                  || lvl_cmp($levels{current}, $worst->{level}) == 0 && $total_seconds > $worst->{time})) {
  1701.     return;
  1702.     }
  1703.  
  1704.     play_sound('applause');
  1705.     append_highscore_level();
  1706.  
  1707.     my %new_entry;
  1708.     $new_entry{level} = $levels{current};
  1709.     $new_entry{time} = $total_seconds;
  1710.     $new_entry{piclevel} = count_highscorehistory_levels();
  1711.     ask_from({ intro => [ 'CONGRATULATIONS!', "YOU HAVE A HIGHSCORE!", '' ],
  1712.            entries => [ { 'q' => 'YOUR NAME?', 'a' => \$new_entry{name} } ],
  1713.            outro => 'GREAT GAME!',
  1714.            erase_background => $background,
  1715.          });
  1716.  
  1717.     return if $new_entry{name} eq '';
  1718.  
  1719.     push @$HISCORES, \%new_entry;
  1720.     if (@$HISCORES == 11) {
  1721.     my @high = ordered_highscores();
  1722.     pop @high;
  1723.     $HISCORES = \@high;
  1724.     }
  1725.  
  1726.     output($hiscorefile, Data::Dumper->Dump([$HISCORES], [qw(HISCORES)]));
  1727.     display_highscores();
  1728. }
  1729.  
  1730. # append the new highscore to the .fbhighlevelshistory
  1731. sub append_highscore_level() {
  1732.  
  1733.     my $row_numb = 0;
  1734.     my $lvl = 1;
  1735.  
  1736.     my @contents;
  1737.  
  1738.     foreach my $line (cat_($loaded_levelset)) {
  1739.     if ($line !~ /\S/) {
  1740.         if ($row_numb) {
  1741.         $lvl++;
  1742.         $row_numb = 0;
  1743.             } 
  1744.         } else {
  1745.             $row_numb++;
  1746.             $lvl == ($levels{current} eq 'WON' ? (keys %levels)-1 : $levels{current})
  1747.           and push @contents, $line;
  1748.         }
  1749.     }
  1750.  
  1751.     append_to_file("$FPATH/.fbhighlevelshistory", @contents, "\n\n");
  1752. }
  1753.  
  1754. sub count_highscorehistory_levels() {
  1755.     my $cnt = 0;
  1756.     my $row_numb = 0;
  1757.     foreach my $line (cat_("$FPATH/.fbhighlevelshistory")) {
  1758.     if ($line !~ /\S/) {
  1759.         if ($row_numb) {
  1760.         $cnt++;
  1761.         $row_numb = 0;
  1762.             } 
  1763.         } else {
  1764.             $row_numb++;
  1765.         }
  1766.     }
  1767.     return $cnt;
  1768.  
  1769.  
  1770. #- ----------- mainloop ---------------------------------------------------
  1771.  
  1772. sub maingame() {
  1773.     my $synchro_ticks = $app->ticks;
  1774.  
  1775.     handle_graphics(\&erase_image);
  1776.     update_game();
  1777.     handle_graphics(\&put_image);
  1778.  
  1779.     $app->update(@update_rects);
  1780.     @update_rects = ();
  1781.  
  1782.     my $to_wait = $TARGET_ANIM_SPEED - ($app->ticks - $synchro_ticks);
  1783.     $to_wait > 0 and fb_c_stuff::fbdelay($to_wait);
  1784. }
  1785.  
  1786.  
  1787. #- ----------- intro stuff ------------------------------------------------
  1788.  
  1789. sub intro() {
  1790.  
  1791.     my %storyboard = (
  1792.               sleeping => {
  1793.                    start => { type => 'time', value => 0 },
  1794.                    type => 'penguin',
  1795.                    animations => [ qw(1 2 3 4 5 6 7 6 5 4 3 2) ],
  1796.                   },
  1797.               music => { start => { type => 'time', value => 1 } },
  1798.               bubble_fall1 => { start => { type => 'synchro', value => 0x01 },
  1799.                     type => 'bubble_falling', img => 2, xpos => 200, xaccel => -1.5 },
  1800.               bubble_fall2 => { start => { type => 'synchro', value => 0x02 },
  1801.                     type => 'bubble_falling', img => 3, xpos => 350, xaccel => 1 },
  1802.               bubble_fall3 => { start => { type => 'synchro', value => 0x03 },
  1803.                     type => 'bubble_falling', img => 4, xpos => 400, xaccel => 2 },
  1804.               eyes_moving => {
  1805.                       start => { type => 'synchro', value => 0x21 },
  1806.                       type => 'penguin',
  1807.                       animations => [ qw(8 9 10 11 12 11 10 9) ],
  1808.                   },
  1809.               arms_moving => {
  1810.                       start => { type => 'synchro', value => 0x22 },
  1811.                       type => 'penguin',
  1812.                       animations => [ qw(12 13 14 15 14 13) ],
  1813.                   },
  1814.               fear => {
  1815.                    start => { type => 'synchro', value => 0x31 },
  1816.                    type => 'penguin',
  1817.                    animations => [ qw(15 16 17 18 19 18 17 16) ],
  1818.                   },
  1819.               txt_frozen_arriving => {
  1820.                           start => { type => 'synchro', value => 0x31 },
  1821.                           type => 'bitmap_animation',
  1822.                           img => $imgbin{frozen},
  1823.                           finalpos => { x => 300, 'y' => 100 },
  1824.                           factor => 1,
  1825.                          },
  1826.               txt_bubble_arriving => {
  1827.                           start => { type => 'synchro', value => 0x32 },
  1828.                           type => 'bitmap_animation',
  1829.                           img => $imgbin{bubble},
  1830.                           finalpos => { x => 340, 'y' => 155 },
  1831.                           factor => 4,
  1832.                          },
  1833.              );
  1834.  
  1835.     my %sb_params = (
  1836.              animation_speed => 20
  1837.             );
  1838.  
  1839.  
  1840.     my $start_menu;
  1841.     my ($slowdown_number, $slowdown_frame);
  1842.  
  1843.     return menu(0);   #- temporarily desactivate the intro storyboard because it's not finished yet
  1844.  
  1845.     if ($mixer_enabled && $mixer) {
  1846.     play_music('intro');
  1847.     $mixer->pause_music;
  1848.     my $back_start = SDL::Surface->new(-name => "$FPATH/intro/back_intro.png");
  1849.     $back_start->blit($apprects{main}, $app, $apprects{main});
  1850.     $app->flip;
  1851.  
  1852.     my $penguin;
  1853.     my @bubbles_falling;
  1854.     my @bitmap_animations;
  1855.  
  1856.     my $anim_step = -1;
  1857.     my $start_time = $app->ticks;
  1858.     my $current_time = $start_time;
  1859.  
  1860.     while (!$start_menu) {
  1861.         my $synchro_ticks = $app->ticks;
  1862.  
  1863.         my $current_time_ = int(($app->ticks - $start_time)/1000);
  1864.         my $anim_step_ = fb_c_stuff::get_synchro_value();
  1865.  
  1866.         if ($anim_step_ != $anim_step || $current_time_ != $current_time) {
  1867.         $anim_step = $anim_step_;
  1868.         $current_time = $current_time_;
  1869.         printf "Anim step: %12s Time: <$current_time>\n", sprintf "<0x%02x>", $anim_step;
  1870.  
  1871.         foreach my $evt (keys %storyboard) {
  1872.             next if $storyboard{$evt}->{already};
  1873.             if ($storyboard{$evt}->{start}->{type} eq 'time' && $storyboard{$evt}->{start}->{value} <= $current_time
  1874.             || $storyboard{$evt}->{start}->{type} eq 'synchro' && $storyboard{$evt}->{start}->{value} eq $anim_step) {
  1875.             $storyboard{$evt}->{already} = 1;
  1876.             print "*** Starting <$evt>\n";
  1877.             $evt eq 'music' and $mixer->resume_music;
  1878.             if ($storyboard{$evt}->{type} eq 'penguin') {
  1879.                 $penguin = { animations => $storyboard{$evt}->{animations},
  1880.                      current_anim => 0,
  1881.                      anim_step => $sb_params{animation_speed} };
  1882.             }
  1883.             if ($storyboard{$evt}->{type} eq 'bubble_falling') {
  1884.                 push @bubbles_falling, { img => $bubbles_images[$storyboard{$evt}->{img}], 'y' => 0, speed => 3,
  1885.                              x => $storyboard{$evt}->{xpos}, xaccel => $storyboard{$evt}->{xaccel} };
  1886.             }
  1887.             if ($storyboard{$evt}->{type} eq 'bitmap_animation') {
  1888.                 push @bitmap_animations, { img => $storyboard{$evt}->{img}, 'y' => 0,
  1889.                                x => $storyboard{$evt}->{finalpos}->{x},
  1890.                                finaly => $storyboard{$evt}->{finalpos}->{'y'},
  1891.                                factor => $storyboard{$evt}->{factor},
  1892.                              };
  1893.             }
  1894.             }
  1895.         }
  1896.  
  1897.         $anim_step == 0x09 and $start_menu = 1;
  1898.         }
  1899.  
  1900.         if ($penguin) {
  1901.         $penguin->{anim_step}++;
  1902.         if ($penguin->{anim_step} >= $sb_params{animation_speed}) {
  1903.             my $img_number = ${$penguin->{animations}}[$penguin->{current_anim}];
  1904.             erase_image_from($imgbin{intro_penguin_imgs}->{$img_number}, 260, 293, $back_start);
  1905.             $penguin->{anim_step} = 0;
  1906.             $penguin->{current_anim}++;
  1907.             $penguin->{current_anim} == @{$penguin->{animations}} and $penguin->{current_anim} = 0;
  1908.             $img_number = ${$penguin->{animations}}[$penguin->{current_anim}];
  1909.             put_image($imgbin{intro_penguin_imgs}->{$img_number}, 260, 293);
  1910.         }
  1911.         }
  1912.  
  1913.         foreach my $b (@bubbles_falling) {
  1914.         erase_image_from($b->{img}, $b->{x}, $b->{'y'}, $back_start);
  1915.         $b->{'x'} += $b->{xaccel};
  1916.         $b->{'y'} += $b->{speed};
  1917.         if ($b->{'y'} >= 360 && !$b->{already_rebound}) {
  1918.             $b->{already_rebound} = 1;
  1919.             $b->{'y'} = 2*360 - $b->{'y'};
  1920.             $b->{speed} *= -0.5;
  1921.         }
  1922.         $b->{speed} += $FREE_FALL_CONSTANT;
  1923.         $b->{kill} = $b->{'y'} > 470;
  1924.         $b->{kill} or put_image($b->{img}, $b->{x}, $b->{'y'});
  1925.         }
  1926.         @bubbles_falling = grep { !$_->{kill} } @bubbles_falling;
  1927.  
  1928.         erase_image_from($_->{img}, $_->{x}, $_->{'y'}, $back_start) foreach @bitmap_animations;
  1929.         foreach my $b (@bitmap_animations) {
  1930.         foreach (0..$slowdown_frame) {
  1931.             $b->{'y'} = $b->{'finaly'} - 200*cos(3*$b->{step})/exp($b->{step}*$b->{step});
  1932.             $b->{step} += 0.015 * $b->{factor};
  1933.         }
  1934.         }
  1935.         $slowdown_frame = 0;
  1936.         put_image($_->{img}, $_->{x}, $_->{'y'}) foreach @bitmap_animations;
  1937.  
  1938.         $app->update(@update_rects);
  1939.         @update_rects = ();
  1940.  
  1941.         my $to_wait = $TARGET_ANIM_SPEED - ($app->ticks - $synchro_ticks);
  1942.         if ($to_wait > 0) {
  1943.         $app->delay($to_wait);
  1944.         } else {
  1945. #        print "slow by: <$to_wait>\n";
  1946.         $slowdown_number += -$to_wait;
  1947.         if ($slowdown_number > $TARGET_ANIM_SPEED) {
  1948.             $slowdown_frame = int($slowdown_number / $TARGET_ANIM_SPEED);
  1949.             $slowdown_number -= $slowdown_frame * $TARGET_ANIM_SPEED;
  1950. #            print "skip frames: <$slowdown_frame>\n";
  1951.         }
  1952.         }
  1953.  
  1954.         $event->pump;
  1955.         $event->poll != 0 && $event->type == SDL_KEYDOWN && member($event->key_sym, (SDLK_RETURN, SDLK_SPACE, SDLK_KP_ENTER, SDLK_ESCAPE))
  1956.         and $start_menu = 2;
  1957.  
  1958.     }
  1959.     }
  1960.  
  1961.  
  1962. #    if ($start_menu == 1) {
  1963. #    my $bkg = SDL::Surface->new(-width => $app->width, -height => $app->height, -depth => 32, -Amask => '0 but true');
  1964. #    $app->blit($apprects{main}, $bkg, $apprects{main});
  1965. #    menu(1, $bkg);
  1966. #    } else {
  1967.     menu(1);
  1968. #    }
  1969. }
  1970.  
  1971.  
  1972. #- ----------- menu stuff -------------------------------------------------
  1973.  
  1974. sub menu {
  1975.     my ($from_intro, $back_from_intro) = @_;
  1976.  
  1977.     handle_new_hiscores();
  1978.  
  1979.     if (!$from_intro) {
  1980.     @playlist or play_music('intro', 8);
  1981.     }
  1982.  
  1983.     my $back_start;
  1984.     if (!$from_intro || !$back_from_intro) {
  1985.     $back_start = $imgbin{backstartfull};
  1986.     $back_start->blit($apprects{main}, $app, $apprects{main});
  1987.     put_image($imgbin{version}, 17, 432);
  1988.     } else {
  1989.     $back_start = $back_from_intro;
  1990.     }
  1991.  
  1992.     my $invalidate_all;
  1993.  
  1994.     my $menu_start_sound = sub {
  1995.     if (!$mixer_enabled && !$mixer && !init_sound()) {
  1996.         return 0;
  1997.     } else {
  1998.         $mixer_enabled = 1;
  1999.         play_music('intro', 8);
  2000.         return 1;
  2001.     }
  2002.     };
  2003.  
  2004.     my $menu_stop_sound = sub {
  2005.     if ($mixer_enabled && $mixer && $mixer->playing_music) {
  2006.         $app->delay(10) while $mixer->fading_music;   #- mikmod will deadlock if we try to fade_out while still fading in
  2007.         $mixer->playing_music and $mixer->fade_out_music(500); $app->delay(450);
  2008.         $app->delay(10) while $mixer->playing_music;  #- mikmod will segfault if we try to load a music while old one is still fading out
  2009.     }
  2010.     $mixer_enabled = undef;
  2011.     return 1;
  2012.     };
  2013.  
  2014.     my $menu_display_highscores = sub {
  2015.     display_highscores();
  2016.  
  2017.     $back_start->blit($apprects{main}, $app, $apprects{main});
  2018.     $app->flip;
  2019.     $invalidate_all->();
  2020.     };
  2021.  
  2022.     my $change_keys = sub {
  2023.     ask_from({ intro => [ 'PLEASE ENTER NEW KEYS' ],
  2024.            entries => [
  2025.                    { 'q' => 'RIGHT-PL/LEFT?',  'a' => \$KEYS->{p2}{left},  f => 'ONE_CHAR' },
  2026.                    { 'q' => 'RIGHT-PL/RIGHT?', 'a' => \$KEYS->{p2}{right}, f => 'ONE_CHAR' },
  2027.                    { 'q' => 'RIGHT-PL/FIRE?',  'a' => \$KEYS->{p2}{fire},  f => 'ONE_CHAR' },
  2028.                    { 'q' => 'RIGHT-PL/CENTER?',  'a' => \$KEYS->{p2}{center},  f => 'ONE_CHAR' },
  2029.                    { 'q' => 'LEFT-PL/LEFT?',  'a' => \$KEYS->{p1}{left},  f => 'ONE_CHAR' },
  2030.                    { 'q' => 'LEFT-PL/RIGHT?', 'a' => \$KEYS->{p1}{right}, f => 'ONE_CHAR' },
  2031.                    { 'q' => 'LEFT-PL/FIRE?',  'a' => \$KEYS->{p1}{fire},  f => 'ONE_CHAR' },
  2032.                    { 'q' => 'LEFT-PL/CENTER?',  'a' => \$KEYS->{p1}{center},  f => 'ONE_CHAR' },
  2033.                    { 'q' => 'TOGGLE FULLSCREEN?', 'a' => \$KEYS->{misc}{fs}, f => 'ONE_CHAR' },
  2034.                   ],
  2035.            outro => 'THANKS!',
  2036.            erase_background => $back_start
  2037.          });
  2038.     $invalidate_all->();
  2039.     };
  2040.  
  2041.     my $launch_editor = sub {
  2042.         SDL::ShowCursor(1);
  2043.         FBLE::init_setup('embedded', $app);
  2044.         FBLE::handle_events();
  2045.         SDL::ShowCursor(0);
  2046.         $back_start->blit($apprects{main}, $app, $apprects{main});
  2047.         $app->flip;
  2048.         $invalidate_all->();
  2049.     };
  2050.     my ($MENU_XPOS, $MENU_FIRSTY, $SPACING) = (56, 30, 51);
  2051.     my %menu_ypos = ( '1pgame' =>      $MENU_FIRSTY,
  2052.               '2pgame' =>      $MENU_FIRSTY +     $SPACING,
  2053.               'editor' =>      $MENU_FIRSTY + 2 * $SPACING,
  2054.               'fullscreen' =>  $MENU_FIRSTY + 3 * $SPACING,
  2055.               'graphics' =>    $MENU_FIRSTY + 4 * $SPACING,
  2056.               'sound' =>       $MENU_FIRSTY + 5 * $SPACING,
  2057.               'keys' =>        $MENU_FIRSTY + 6 * $SPACING,
  2058.               'highscores' =>  $MENU_FIRSTY + 7 * $SPACING,
  2059.           );
  2060.     my %menu_entries = ( '1pgame' => { pos => 1, type => 'rungame',
  2061.                        run => sub { @PLAYERS = ('p1'); $levels{current} = 1; $time_1pgame = $app->ticks } },
  2062.              '2pgame' => { pos => 2, type => 'rungame',
  2063.                        run => sub { @PLAYERS = qw(p1 p2); $levels{current} = undef; } },
  2064.              'editor' => { pos => 3, type => 'run', run => sub { $launch_editor->(); } },
  2065.              'fullscreen' => { pos => 4, type => 'toggle',
  2066.                        #act => sub { $fullscreen = 1; $app->fullscreen },
  2067.                        act => sub { },
  2068.                        unact => sub { $fullscreen = 0; $app->fullscreen },
  2069.                        value => $fullscreen },
  2070.              'graphics' => { pos => 5, type => 'range', valuemin => 1, valuemax => 3,
  2071.                      change => sub { $graphics_level = $_[0] }, value => $graphics_level },
  2072.              'sound' => { pos => 6, type => 'toggle',
  2073.                       act => sub { $menu_start_sound->() },
  2074.                       unact => sub { $menu_stop_sound->() },
  2075.                       value => $mixer_enabled },
  2076.              'keys' => { pos => 7, type => 'run',
  2077.                      run => sub { $change_keys->() } },
  2078.              'highscores' => { pos => 8, type => 'run',
  2079.                        run => sub { $menu_display_highscores->() } },
  2080.                );
  2081.     my $current_pos if 0; $current_pos ||= 1;
  2082.     my @menu_invalids;
  2083.     $invalidate_all = sub { push @menu_invalids, $menu_entries{$_}->{pos} foreach keys %menu_entries };
  2084.  
  2085.     my $menu_update = sub {
  2086.     @update_rects = ();
  2087.     foreach my $m (keys %menu_entries) {
  2088.         member($menu_entries{$m}->{pos}, @menu_invalids) or next;
  2089.         my $txt = "txt_$m";
  2090.         $menu_entries{$m}->{type} eq 'toggle' && $menu_entries{$m}->{value} and $txt .= "_act";
  2091.         $menu_entries{$m}->{type} eq 'range' and $txt .= "_$menu_entries{$m}->{value}";
  2092.         $txt .= $menu_entries{$m}->{pos} == $current_pos ? '_over' : '_off';
  2093.         erase_image_from($imgbin{$txt}, $MENU_XPOS, $menu_ypos{$m}, $back_start);
  2094.         put_image($imgbin{$txt}, $MENU_XPOS, $menu_ypos{$m});
  2095.     }
  2096.     @menu_invalids = ();
  2097.     $app->update(@update_rects);
  2098.     };
  2099.  
  2100.     $app->flip;
  2101.     $invalidate_all->();
  2102.     $menu_update->();
  2103.     $event->pump while ($event->poll != 0);
  2104.  
  2105.     my $start_game = 0;
  2106.     my ($BANNER_START, $BANNER_SPACING) = (720, 80);
  2107.     my %banners = (artwork => $BANNER_START,
  2108.            soundtrack => $BANNER_START + $imgbin{banner_artwork}->width + $BANNER_SPACING,
  2109.            cpucontrol => $BANNER_START + $imgbin{banner_artwork}->width + $BANNER_SPACING
  2110.                          + $imgbin{banner_soundtrack}->width + $BANNER_SPACING,
  2111.            leveleditor => $BANNER_START + $imgbin{banner_artwork}->width + $BANNER_SPACING
  2112.                                  + $imgbin{banner_soundtrack}->width + $BANNER_SPACING
  2113.                                  + $imgbin{banner_cpucontrol}->width + $BANNER_SPACING);
  2114.     my ($BANNER_MINX, $BANNER_MAXX, $BANNER_Y) = (81, 292, 443);
  2115.     my $banners_max = $banners{leveleditor} - (640 - ($BANNER_MAXX - $BANNER_MINX)) + $BANNER_SPACING;
  2116.     my $banner_rect = SDL::Rect->new(-width => $BANNER_MAXX-$BANNER_MINX, -height => 30, '-x' => $BANNER_MINX, '-y' => $BANNER_Y);
  2117.  
  2118.     while (!$start_game) {
  2119.     my $synchro_ticks = $app->ticks;
  2120.  
  2121.     $graphics_level > 1 and $back_start->blit($banner_rect, $app, $banner_rect);
  2122.  
  2123.     $event->pump;
  2124.     if ($event->poll != 0) {
  2125.         if ($event->type == SDL_KEYDOWN) {
  2126.         my $keypressed = $event->key_sym;
  2127.         if (member($keypressed, (SDLK_DOWN, SDLK_RIGHT)) && $current_pos < max(map { $menu_entries{$_}->{pos} } keys %menu_entries)) {
  2128.             $current_pos++;
  2129.             push @menu_invalids, $current_pos-1, $current_pos;
  2130.             play_sound('menu_change');
  2131.         }
  2132.         if (member($keypressed, (SDLK_UP, SDLK_LEFT)) && $current_pos > 1) {
  2133.             $current_pos--;
  2134.             push @menu_invalids, $current_pos, $current_pos+1;
  2135.             play_sound('menu_change');
  2136.         }
  2137.  
  2138.         if (member($keypressed, (SDLK_RETURN, SDLK_SPACE, SDLK_KP_ENTER))) {
  2139.             play_sound('menu_selected');
  2140.             push @menu_invalids, $current_pos;
  2141.             foreach my $m (keys %menu_entries) {
  2142.             if ($menu_entries{$m}->{pos} == $current_pos) {
  2143.                 if ($menu_entries{$m}->{type} =~ /^run/) {
  2144.                 $menu_entries{$m}->{run}->();
  2145.                 $menu_entries{$m}->{type} eq 'rungame' and $start_game = 1;
  2146.                 }
  2147.                 if ($menu_entries{$m}->{type} eq 'toggle') {
  2148.                 $menu_entries{$m}->{value} = !$menu_entries{$m}->{value};
  2149.                 if ($menu_entries{$m}->{value}) {
  2150.                     $menu_entries{$m}->{act}->() or $menu_entries{$m}->{value} = 0;
  2151.                 } else {
  2152.                     $menu_entries{$m}->{unact}->() or $menu_entries{$m}->{value} = 1;
  2153.                 }
  2154.                 }
  2155.                 if ($menu_entries{$m}->{type} eq 'range') {
  2156.                 $menu_entries{$m}->{value}++;
  2157.                 $menu_entries{$m}->{value} > $menu_entries{$m}->{valuemax}
  2158.                   and $menu_entries{$m}->{value} = $menu_entries{$m}->{valuemin};
  2159.                 $menu_entries{$m}->{change}->($menu_entries{$m}->{value});
  2160.                 }
  2161.             }
  2162.             }
  2163.         }
  2164.  
  2165.         if ($keypressed == SDLK_ESCAPE) {
  2166. #            print "*** p1\n\n";
  2167. #            history('p1');
  2168. #            print "*** p2\n\n";
  2169. #            history('p2');
  2170.             exit 0;
  2171.         }
  2172.         }
  2173.         $menu_update->();
  2174.     }
  2175.  
  2176.     if ($graphics_level > 1) {
  2177.         my $banner_pos if 0;
  2178.         $banner_pos ||= 670;
  2179.         foreach my $b (keys %banners) {
  2180.         my $xpos = $banners{$b} - $banner_pos;
  2181.         my $image = $imgbin{"banner_$b"};
  2182.  
  2183.         $xpos > $banners_max/2 and $xpos = $banners{$b} - ($banner_pos + $banners_max);
  2184.  
  2185.         if ($xpos < $BANNER_MAXX && $xpos + $image->width >= 0) {
  2186.             my $irect = SDL::Rect->new(-width => min($image->width+$xpos, $BANNER_MAXX-$BANNER_MINX), -height => $image->height, -x => -$xpos);
  2187.             $image->blit($irect, $app, SDL::Rect->new(-x => $BANNER_MINX, '-y' => $BANNER_Y));
  2188.         }
  2189.         }
  2190.         $banner_pos++;
  2191.         $banner_pos >= $banners_max and $banner_pos = 1;
  2192.     }
  2193.     $app->update($banner_rect);
  2194.  
  2195.     my $to_wait = $TARGET_ANIM_SPEED - ($app->ticks - $synchro_ticks);
  2196.     $to_wait > 0 and $app->delay($to_wait);
  2197.     }
  2198.  
  2199.     #- for $KEYS, try hard to keep SDLK_<key> instead of integer value in rcfile
  2200.     my $KEYS_;
  2201.     foreach my $p (keys %$KEYS) {
  2202.     foreach my $k (keys %{$KEYS->{$p}}) {
  2203.         eval("$KEYS->{$p}->{$k} eq SDLK_$_") and $KEYS_->{$p}->{$k} = "SDLK_$_" foreach @fbsyms::syms;
  2204.     }
  2205.     }
  2206.     my $dump = Data::Dumper->Dump([$fullscreen, $graphics_level, $KEYS_], [qw(fullscreen graphics_level KEYS)]);
  2207.     $dump =~ s/'SDLK_(\w+)'/SDLK_$1/g;
  2208.     output($rcfile, $dump);
  2209.  
  2210.     iter_players {
  2211.     !is_1p_game() and $pdata{$::p}{score} = 0;
  2212.     };
  2213. }
  2214.  
  2215.  
  2216. #- ----------- editor stuff --------------------------------------------
  2217.  
  2218. sub choose_levelset() {
  2219.  
  2220.     my @levelsets = sort glob("$FBLEVELS/*");
  2221.  
  2222.     if ($direct_levelset) {
  2223.         load_levelset("$FBLEVELS/$direct_levelset");
  2224.         $direct_levelset = '';
  2225.  
  2226.     } elsif (!@levelsets) {
  2227.         # no .fblevels directory or void directory, just return and let the
  2228.         # game continue (means that the level editor has never been opened)
  2229.  
  2230.     } else {
  2231.     
  2232.     if (@levelsets <= 1) {
  2233.         load_levelset($levelsets[0]);
  2234.     } else {
  2235.         FBLE::init_app('embedded', $app);
  2236.         FBLE::create_play_levelset_dialog();
  2237.         SDL::ShowCursor(1);
  2238.         my $play_level = FBLE::handle_events();
  2239.         load_levelset("$FBLEVELS/$play_level");
  2240.         SDL::ShowCursor(0);
  2241.     }
  2242.     }
  2243. }
  2244.  
  2245.  
  2246. #- ----------- main -------------------------------------------------------
  2247.  
  2248. init_game();
  2249.  
  2250. $direct or intro();
  2251.  
  2252. new_game_once();
  2253. new_game();
  2254.  
  2255.  
  2256. while (1) {
  2257.     eval { maingame() };
  2258.     if ($@) {
  2259.     if ($@ =~ /^new_game/) {
  2260.         new_game();
  2261.     } elsif ($@ =~ /^quit/) {
  2262.         menu();
  2263.         new_game_once();
  2264.         new_game();
  2265.     } else {
  2266.         die;
  2267.     }
  2268.     }
  2269. }
  2270.